Vault issuer support
vault remove duration
This commit is contained in:
parent
230f59b0ac
commit
b35343786e
@ -17,6 +17,7 @@ import (
|
|||||||
_ "github.com/jetstack/cert-manager/pkg/controller/issuers"
|
_ "github.com/jetstack/cert-manager/pkg/controller/issuers"
|
||||||
_ "github.com/jetstack/cert-manager/pkg/issuer/acme"
|
_ "github.com/jetstack/cert-manager/pkg/issuer/acme"
|
||||||
_ "github.com/jetstack/cert-manager/pkg/issuer/ca"
|
_ "github.com/jetstack/cert-manager/pkg/issuer/ca"
|
||||||
|
_ "github.com/jetstack/cert-manager/pkg/issuer/vault"
|
||||||
"github.com/jetstack/cert-manager/pkg/util"
|
"github.com/jetstack/cert-manager/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
4
contrib/charts/vault/Chart.yaml
Normal file
4
contrib/charts/vault/Chart.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
name: vault
|
||||||
|
version: 0.1.0
|
||||||
16
contrib/charts/vault/templates/_helpers.tpl
Normal file
16
contrib/charts/vault/templates/_helpers.tpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{{/* vim: set filetype=mustache: */}}
|
||||||
|
{{/*
|
||||||
|
Expand the name of the chart.
|
||||||
|
*/}}
|
||||||
|
{{- define "name" -}}
|
||||||
|
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
|
|
||||||
|
{{/*
|
||||||
|
Create a default fully qualified app name.
|
||||||
|
We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
|
||||||
|
*/}}
|
||||||
|
{{- define "fullname" -}}
|
||||||
|
{{- $name := default .Chart.Name .Values.nameOverride -}}
|
||||||
|
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}}
|
||||||
|
{{- end -}}
|
||||||
34
contrib/charts/vault/templates/vault-deployment.yaml
Normal file
34
contrib/charts/vault/templates/vault-deployment.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
apiVersion: extensions/v1beta1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: vault
|
||||||
|
name: vault
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: vault
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: vault
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
ports:
|
||||||
|
- containerPort: 8200
|
||||||
|
name: vaultport
|
||||||
|
protocol: TCP
|
||||||
|
securityContext:
|
||||||
|
capabilities:
|
||||||
|
add:
|
||||||
|
- IPC_LOCK
|
||||||
|
env:
|
||||||
|
- name: VAULT_DEV_ROOT_TOKEN_ID
|
||||||
|
value: vault-root-token
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /v1/sys/health
|
||||||
|
port: 8200
|
||||||
12
contrib/charts/vault/templates/vault-service.yaml
Normal file
12
contrib/charts/vault/templates/vault-service.yaml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: vault
|
||||||
|
labels:
|
||||||
|
app: vault
|
||||||
|
spec:
|
||||||
|
ports:
|
||||||
|
- name: vault
|
||||||
|
port: 8200
|
||||||
|
selector:
|
||||||
|
app: vault
|
||||||
4
contrib/charts/vault/values.yaml
Normal file
4
contrib/charts/vault/values.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
image:
|
||||||
|
repository: vault
|
||||||
|
tag: "0.9.3"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
@ -9,7 +9,7 @@ Welcome to cert-manager's documentation!
|
|||||||
|
|
||||||
cert-manager is a native Kubernetes_ certificate management controller.
|
cert-manager is a native Kubernetes_ certificate management controller.
|
||||||
It can help with issuing certificates from a variety of sources, such as
|
It can help with issuing certificates from a variety of sources, such as
|
||||||
`Let's Encrypt`_ or a simple signing keypair.
|
`Let's Encrypt`_, `HashiCorp Vault`_ or a simple signing keypair.
|
||||||
|
|
||||||
It will ensure certificates are valid and up to date, and attempt to renew
|
It will ensure certificates are valid and up to date, and attempt to renew
|
||||||
certificates at a configured time before expiry.
|
certificates at a configured time before expiry.
|
||||||
@ -37,3 +37,4 @@ a source of references when seeking help with the project.
|
|||||||
.. _kube-lego: https://github.com/jetstack/kube-lego
|
.. _kube-lego: https://github.com/jetstack/kube-lego
|
||||||
.. _kube-cert-manager: https://github.com/PalmStoneGames/kube-cert-manager
|
.. _kube-cert-manager: https://github.com/PalmStoneGames/kube-cert-manager
|
||||||
.. _`Let's Encrypt`: https://letsencrypt.org
|
.. _`Let's Encrypt`: https://letsencrypt.org
|
||||||
|
.. _`HashiCorp Vault`: https://www.vaultproject.io
|
||||||
|
|||||||
@ -126,6 +126,8 @@ Name Description
|
|||||||
:doc:`CA <issuers/ca/index>` Supports issuing certificates using a
|
:doc:`CA <issuers/ca/index>` Supports issuing certificates using a
|
||||||
simple signing keypair, stored in a Secret
|
simple signing keypair, stored in a Secret
|
||||||
in the Kubernetes API server
|
in the Kubernetes API server
|
||||||
|
:doc:`Vault <issuers/vault/index>` Supports issuing certificates using
|
||||||
|
HashiCorp Vault.
|
||||||
=================================== =========================================
|
=================================== =========================================
|
||||||
|
|
||||||
Each Issuer resource is of one, and only one type. The type of an Issuer is
|
Each Issuer resource is of one, and only one type. The type of an Issuer is
|
||||||
@ -136,6 +138,7 @@ for the ACME issuer, or ``spec.ca`` for the CA based issuer.
|
|||||||
|
|
||||||
issuers/acme/index
|
issuers/acme/index
|
||||||
issuers/ca/index
|
issuers/ca/index
|
||||||
|
issuers/vault/index
|
||||||
|
|
||||||
.. _`Let's Encrypt`: https://letsencrypt.org
|
.. _`Let's Encrypt`: https://letsencrypt.org
|
||||||
.. _kube2iam: https://github.com/jtblin/kube2iam
|
.. _kube2iam: https://github.com/jtblin/kube2iam
|
||||||
|
|||||||
5
docs/reference/issuers/vault/OWNERS
Normal file
5
docs/reference/issuers/vault/OWNERS
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
approvers:
|
||||||
|
- munnerz
|
||||||
|
reviewers:
|
||||||
|
- munnerz
|
||||||
|
- vdesjardins
|
||||||
15
docs/reference/issuers/vault/index.rst
Normal file
15
docs/reference/issuers/vault/index.rst
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
===================
|
||||||
|
Vault Configuration
|
||||||
|
===================
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
Vault Issuers issue certificates from `Hashicorp's
|
||||||
|
Vault <https://www.vaultproject.io/>`__.
|
||||||
|
|
||||||
|
You can find user guides on using the Vault Issuer in the :doc:`Vault Issuer tutorials
|
||||||
|
section </tutorials/vault/index>`.
|
||||||
|
|
||||||
|
.. todo::
|
||||||
|
Expand out Vault Issuer reference documentation
|
||||||
@ -10,3 +10,4 @@ cert-manager.
|
|||||||
|
|
||||||
acme/index
|
acme/index
|
||||||
ca/index
|
ca/index
|
||||||
|
vault/index
|
||||||
|
|||||||
5
docs/tutorials/vault/OWNERS
Normal file
5
docs/tutorials/vault/OWNERS
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
approvers:
|
||||||
|
- munnerz
|
||||||
|
reviewers:
|
||||||
|
- munnerz
|
||||||
|
- vdesjardins
|
||||||
192
docs/tutorials/vault/creating-vault-issuers.rst
Normal file
192
docs/tutorials/vault/creating-vault-issuers.rst
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
Vault Installation
|
||||||
|
==================
|
||||||
|
|
||||||
|
Installing Vault
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Vault installation is a complex subject. For a thorough tour of the subject
|
||||||
|
you can read the official HashiCorp Vault
|
||||||
|
`documentation <https://www.vaultproject.io/intro/getting-started/install.html>`__.
|
||||||
|
|
||||||
|
|
||||||
|
Vault PKI Backend
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
The PKI Secrets Engine needs to be initialized for cert-manager to be
|
||||||
|
able to generate certificate. The official Vault documentation can be
|
||||||
|
found
|
||||||
|
`here <https://www.vaultproject.io/docs/secrets/pki/index.html>`__.
|
||||||
|
|
||||||
|
Vault Authentication with a AppRole
|
||||||
|
===================================
|
||||||
|
|
||||||
|
This Vault authentication method uses a
|
||||||
|
`Vault AppRole <https://www.vaultproject.io/docs/auth/approle.html>`__.
|
||||||
|
|
||||||
|
The secret ID of the AppRole is stored in a secret.
|
||||||
|
|
||||||
|
Here an example of a secret containing the secretId of the AppRole:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
type: Opaque
|
||||||
|
metadata:
|
||||||
|
name: cert-manager-vault-approle
|
||||||
|
namespace: default
|
||||||
|
data:
|
||||||
|
secretId: "MDI..."
|
||||||
|
|
||||||
|
Where the secretId is the base 64 encoded value of the appRole *secretId*
|
||||||
|
giving access to the pki backend in Vault.
|
||||||
|
|
||||||
|
We can now create a cluster issuer referencing this secret:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: certmanager.k8s.io/v1alpha1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: vault-issuer
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
vault:
|
||||||
|
path: pki_int/sign/example-dot-com
|
||||||
|
server: https://vault
|
||||||
|
auth:
|
||||||
|
appRole:
|
||||||
|
roleId: "291b9d21-8ff5-..."
|
||||||
|
secretRef:
|
||||||
|
name: cert-manager-vault-approle
|
||||||
|
key: secretId
|
||||||
|
|
||||||
|
Where *path* is the Vault role path of the PKI backend and *server* is
|
||||||
|
the Vault server base URL. The Vault appRole credentials are supplied as the
|
||||||
|
Vault authentication method using the appRole created in Vault. The secretRef
|
||||||
|
references the Kubernetes secret created previously. More specifically, the field
|
||||||
|
*name* is the Kubernetes secret name and *key* is the name given as the
|
||||||
|
key value that store the *secretId*.
|
||||||
|
|
||||||
|
Once we have created the above Issuer we can use it to obtain a certificate.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: certmanager.k8s.io/v1alpha1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: example-com
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
secretName: example-com-tls
|
||||||
|
issuerRef:
|
||||||
|
name: vault-issuer
|
||||||
|
commonName: example.com
|
||||||
|
dnsNames:
|
||||||
|
- www.example.com
|
||||||
|
|
||||||
|
The Certificate resource describes our desired certificate and the possible
|
||||||
|
methods that can be used to obtain it. You can learn more about the Certificate
|
||||||
|
resource in the :doc:`reference docs </reference/certificates>`.
|
||||||
|
If the certificate is obtained successfully, the resulting key pair will be
|
||||||
|
stored in a secret called ``example-com-tls`` in the same namespace as the Certificate.
|
||||||
|
|
||||||
|
The certificate will have a common name of ``example.com`` and the
|
||||||
|
`Subject Alternative Names`_ (SANs) will be ``example.com`` and ``www.example.com``.
|
||||||
|
|
||||||
|
In our Certificate we have referenced the ``vault-issuer`` Issuer above.
|
||||||
|
The Issuer must be in the same namespace as the Certificate.
|
||||||
|
If you want to reference a ClusterIssuer, which is a cluster-scoped version of
|
||||||
|
an Issuer, you must add ``kind: ClusterIssuer`` to the ``issuerRef`` stanza.
|
||||||
|
|
||||||
|
For more information on ClusterIssuers, read the
|
||||||
|
:doc:`ClusterIssuer reference docs </reference/clusterissuers>`.
|
||||||
|
|
||||||
|
Vault Authentication with a Token
|
||||||
|
=================================
|
||||||
|
|
||||||
|
This Vault authentication method uses a plain token. A Vault token is generated by
|
||||||
|
one of the many authentication backend supported by Vault. Tokens in Vault have
|
||||||
|
expiration and need to be refreshed. You need to be aware that cert-manager do not
|
||||||
|
refresh these tokens. Another process must be put in place to keep them from expiring.
|
||||||
|
|
||||||
|
For testing purpose a root token which do not expire is generated at Vault installation
|
||||||
|
time. **WARNING: a root token should only be used for testing purpose only**.
|
||||||
|
|
||||||
|
Please refer to the official token `documentation <https://www.vaultproject.io/docs/concepts/tokens.html>`__
|
||||||
|
for all the details.
|
||||||
|
|
||||||
|
Here an example of a secret Kubernetes resource containing the Vault token:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
type: Opaque
|
||||||
|
metadata:
|
||||||
|
name: cert-manager-vault-token
|
||||||
|
namespace: kube-system
|
||||||
|
data:
|
||||||
|
token: "MjI..."
|
||||||
|
|
||||||
|
Where the token value is the base 64 encoded value of the token giving
|
||||||
|
access to the PKI backend in Vault.
|
||||||
|
|
||||||
|
We can now create an issuer referencing this secret:
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: certmanager.k8s.io/v1alpha1
|
||||||
|
kind: Issuer
|
||||||
|
metadata:
|
||||||
|
name: vault-issuer
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
vault:
|
||||||
|
auth:
|
||||||
|
tokenSecretRef:
|
||||||
|
name: cert-manager-vault-token
|
||||||
|
key: token
|
||||||
|
path: pki_int/sign/example-dot-com
|
||||||
|
server: https://vault
|
||||||
|
|
||||||
|
Where *path* is the Vault role path of the PKI backend and *server* is
|
||||||
|
the Vault server base URL. The secret created previously is referenced in the issuer
|
||||||
|
with its *name* and *key* corresponding to the name of the Kubernetes secret and the
|
||||||
|
property name containing the token value respectively.
|
||||||
|
|
||||||
|
Once we have created the above Issuer we can use it to obtain a certificate.
|
||||||
|
|
||||||
|
.. code-block:: yaml
|
||||||
|
|
||||||
|
apiVersion: certmanager.k8s.io/v1alpha1
|
||||||
|
kind: Certificate
|
||||||
|
metadata:
|
||||||
|
name: example-com
|
||||||
|
namespace: default
|
||||||
|
spec:
|
||||||
|
secretName: example-com-tls
|
||||||
|
issuerRef:
|
||||||
|
name: vault-issuer
|
||||||
|
commonName: example.com
|
||||||
|
dnsNames:
|
||||||
|
- www.example.com
|
||||||
|
|
||||||
|
The Certificate resource describes our desired certificate and the possible
|
||||||
|
methods that can be used to obtain it. You can learn more about the Certificate
|
||||||
|
resource in the :doc:`reference docs </reference/certificates>`.
|
||||||
|
If the certificate is obtained successfully, the resulting key pair will be
|
||||||
|
stored in a secret called ``example-com-tls`` in the same namespace as the Certificate.
|
||||||
|
|
||||||
|
The certificate will have a common name of ``example.com`` and the
|
||||||
|
`Subject Alternative Names`_ (SANs) will be ``example.com`` and ``www.example.com``.
|
||||||
|
|
||||||
|
In our Certificate we have referenced the ``vault-issuer`` Issuer above.
|
||||||
|
The Issuer must be in the same namespace as the Certificate.
|
||||||
|
If you want to reference a ClusterIssuer, which is a cluster-scoped version of
|
||||||
|
an Issuer, you must add ``kind: ClusterIssuer`` to the ``issuerRef`` stanza.
|
||||||
|
|
||||||
|
For more information on ClusterIssuers, read the
|
||||||
|
:doc:`ClusterIssuer reference docs </reference/clusterissuers>`.
|
||||||
|
|
||||||
|
.. _`Subject Alternative Names`: https://en.wikipedia.org/wiki/Subject_Alternative_Name
|
||||||
12
docs/tutorials/vault/index.rst
Normal file
12
docs/tutorials/vault/index.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
======================
|
||||||
|
Vault Issuer Tutorials
|
||||||
|
======================
|
||||||
|
|
||||||
|
This section contains tutorials that specifically utilise the Vault Issuer. They
|
||||||
|
are designed to help teach the underlying concepts of cert-manager whilst also
|
||||||
|
helping 'quickstart' common use-cases for the project.
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 1
|
||||||
|
|
||||||
|
creating-vault-issuers
|
||||||
@ -81,8 +81,34 @@ type IssuerSpec struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IssuerConfig struct {
|
type IssuerConfig struct {
|
||||||
ACME *ACMEIssuer `json:"acme,omitempty"`
|
ACME *ACMEIssuer `json:"acme,omitempty"`
|
||||||
CA *CAIssuer `json:"ca,omitempty"`
|
CA *CAIssuer `json:"ca,omitempty"`
|
||||||
|
Vault *VaultIssuer `json:"vault,omitempty""`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaultIssuer struct {
|
||||||
|
// Vault authentication
|
||||||
|
Auth VaultAuth `json:"auth"`
|
||||||
|
// Server is the vault connection address
|
||||||
|
Server string `json:"server"`
|
||||||
|
// Vault URL path to the certificate role
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vault authentication can be configured:
|
||||||
|
// - With a secret containing a token. Cert-manager is using this token as-is.
|
||||||
|
// - With a secret containing a AppRole. This AppRole is used to authenticate to
|
||||||
|
// Vault and retrieve a token.
|
||||||
|
type VaultAuth struct {
|
||||||
|
// This Secret contains the Vault token key
|
||||||
|
TokenSecretRef SecretKeySelector `json:"tokenSecretRef,omitempty"`
|
||||||
|
// This Secret contains a AppRole and Secret
|
||||||
|
AppRole VaultAppRole `json:"appRoleSecret,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaultAppRole struct {
|
||||||
|
RoleId string `json:"roleId"`
|
||||||
|
SecretRef SecretKeySelector `json:"appRoleSecretRef"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CAIssuer struct {
|
type CAIssuer struct {
|
||||||
|
|||||||
@ -728,6 +728,15 @@ func (in *IssuerConfig) DeepCopyInto(out *IssuerConfig) {
|
|||||||
**out = **in
|
**out = **in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if in.Vault != nil {
|
||||||
|
in, out := &in.Vault, &out.Vault
|
||||||
|
if *in == nil {
|
||||||
|
*out = nil
|
||||||
|
} else {
|
||||||
|
*out = new(VaultIssuer)
|
||||||
|
**out = **in
|
||||||
|
}
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -872,3 +881,55 @@ func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
|
|||||||
in.DeepCopyInto(out)
|
in.DeepCopyInto(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *VaultAppRole) DeepCopyInto(out *VaultAppRole) {
|
||||||
|
*out = *in
|
||||||
|
out.SecretRef = in.SecretRef
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAppRole.
|
||||||
|
func (in *VaultAppRole) DeepCopy() *VaultAppRole {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(VaultAppRole)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *VaultAuth) DeepCopyInto(out *VaultAuth) {
|
||||||
|
*out = *in
|
||||||
|
out.TokenSecretRef = in.TokenSecretRef
|
||||||
|
out.AppRole = in.AppRole
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultAuth.
|
||||||
|
func (in *VaultAuth) DeepCopy() *VaultAuth {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(VaultAuth)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||||
|
func (in *VaultIssuer) DeepCopyInto(out *VaultIssuer) {
|
||||||
|
*out = *in
|
||||||
|
out.Auth = in.Auth
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VaultIssuer.
|
||||||
|
func (in *VaultIssuer) DeepCopy() *VaultIssuer {
|
||||||
|
if in == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := new(VaultIssuer)
|
||||||
|
in.DeepCopyInto(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@ func (c *Controller) issuersForSecret(secret *corev1.Secret) ([]*v1alpha1.Cluste
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (iss.Spec.ACME != nil && iss.Spec.ACME.PrivateKey.Name == secret.Name) ||
|
if (iss.Spec.ACME != nil && iss.Spec.ACME.PrivateKey.Name == secret.Name) ||
|
||||||
(iss.Spec.CA != nil && iss.Spec.CA.SecretName == secret.Name) {
|
(iss.Spec.CA != nil && iss.Spec.CA.SecretName == secret.Name) ||
|
||||||
|
(iss.Spec.Vault != nil && iss.Spec.Vault.Auth.TokenSecretRef.Name == secret.Name) {
|
||||||
affected = append(affected, iss)
|
affected = append(affected, iss)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,7 +22,8 @@ func (c *Controller) issuersForSecret(secret *corev1.Secret) ([]*v1alpha1.Issuer
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (iss.Spec.ACME != nil && iss.Spec.ACME.PrivateKey.Name == secret.Name) ||
|
if (iss.Spec.ACME != nil && iss.Spec.ACME.PrivateKey.Name == secret.Name) ||
|
||||||
(iss.Spec.CA != nil && iss.Spec.CA.SecretName == secret.Name) {
|
(iss.Spec.CA != nil && iss.Spec.CA.SecretName == secret.Name) ||
|
||||||
|
(iss.Spec.Vault != nil && iss.Spec.Vault.Auth.TokenSecretRef.Name == secret.Name) {
|
||||||
affected = append(affected, iss)
|
affected = append(affected, iss)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ const (
|
|||||||
IssuerACME string = "acme"
|
IssuerACME string = "acme"
|
||||||
// IssuerCA is the name of the simple issuer
|
// IssuerCA is the name of the simple issuer
|
||||||
IssuerCA string = "ca"
|
IssuerCA string = "ca"
|
||||||
|
// IssuerVault is the name of the Vault issuer
|
||||||
|
IssuerVault string = "vault"
|
||||||
)
|
)
|
||||||
|
|
||||||
// nameForIssuer determines the name of the issuer implementation given an
|
// nameForIssuer determines the name of the issuer implementation given an
|
||||||
@ -21,6 +23,8 @@ func nameForIssuer(i v1alpha1.GenericIssuer) (string, error) {
|
|||||||
return IssuerACME, nil
|
return IssuerACME, nil
|
||||||
case i.GetSpec().CA != nil:
|
case i.GetSpec().CA != nil:
|
||||||
return IssuerCA, nil
|
return IssuerCA, nil
|
||||||
|
case i.GetSpec().Vault != nil:
|
||||||
|
return IssuerVault, nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("no issuer specified for Issuer '%s/%s'", i.GetObjectMeta().Namespace, i.GetObjectMeta().Name)
|
return "", fmt.Errorf("no issuer specified for Issuer '%s/%s'", i.GetObjectMeta().Namespace, i.GetObjectMeta().Name)
|
||||||
}
|
}
|
||||||
|
|||||||
5
pkg/issuer/vault/OWNERS
Normal file
5
pkg/issuer/vault/OWNERS
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
approvers:
|
||||||
|
- munnerz
|
||||||
|
reviewers:
|
||||||
|
- munnerz
|
||||||
|
- vdesjardins
|
||||||
280
pkg/issuer/vault/issue.go
Normal file
280
pkg/issuer/vault/issue.go
Normal file
@ -0,0 +1,280 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
vault "github.com/hashicorp/vault/api"
|
||||||
|
"github.com/hashicorp/vault/helper/certutil"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/util/errors"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/util/kube"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/util/pki"
|
||||||
|
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorGetCertKeyPair = "ErrGetCertKeyPair"
|
||||||
|
errorIssueCert = "ErrIssueCert"
|
||||||
|
|
||||||
|
successCertIssued = "CertIssueSuccess"
|
||||||
|
|
||||||
|
messageErrorIssueCert = "Error issuing TLS certificate: "
|
||||||
|
|
||||||
|
messageCertIssued = "Certificate issued successfully"
|
||||||
|
|
||||||
|
defaultCertificateDuration = time.Hour * 24 * 90
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultOrganization = "cert-manager"
|
||||||
|
|
||||||
|
keyBitSize = 2048
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *Vault) Issue(ctx context.Context, crt *v1alpha1.Certificate) ([]byte, []byte, error) {
|
||||||
|
key, certPem, err := v.obtainCertificate(ctx, crt)
|
||||||
|
if err != nil {
|
||||||
|
s := messageErrorIssueCert + err.Error()
|
||||||
|
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorIssueCert, s, false)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionTrue, successCertIssued, messageCertIssued, true)
|
||||||
|
|
||||||
|
return key, certPem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) obtainCertificate(ctx context.Context, crt *v1alpha1.Certificate) ([]byte, []byte, error) {
|
||||||
|
// get existing certificate private key
|
||||||
|
signeeKey, err := kube.SecretTLSKey(v.secretsLister, crt.Namespace, crt.Spec.SecretName)
|
||||||
|
if k8sErrors.IsNotFound(err) || errors.IsInvalidData(err) {
|
||||||
|
signeeKey, err = pki.GenerateRSAPrivateKey(keyBitSize)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error generating private key: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("error getting certificate private key: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
commonName := crt.Spec.CommonName
|
||||||
|
altNames := crt.Spec.DNSNames
|
||||||
|
if len(commonName) == 0 && len(altNames) == 0 {
|
||||||
|
return nil, nil, fmt.Errorf("no domains specified on certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
crtPem, err := v.signCertificate(crt, signeeKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return pki.EncodePKCS1PrivateKey(signeeKey), crtPem, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signCertificate returns a signed x509.Certificate object for the given
|
||||||
|
// *v1alpha1.Certificate crt.
|
||||||
|
func (v *Vault) signCertificate(crt *v1alpha1.Certificate, key *rsa.PrivateKey) ([]byte, error) {
|
||||||
|
commonName := pki.CommonNameForCertificate(crt)
|
||||||
|
altNames := pki.DNSNamesForCertificate(crt)
|
||||||
|
|
||||||
|
if len(commonName) == 0 && len(altNames) > 0 {
|
||||||
|
commonName = altNames[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
template := pki.GenerateCSR(commonName, altNames...)
|
||||||
|
template.Subject.Organization = []string{defaultOrganization}
|
||||||
|
|
||||||
|
derBytes, err := x509.CreateCertificateRequest(rand.Reader, template, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error creating x509 certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
pemRequestBuf := &bytes.Buffer{}
|
||||||
|
err = pem.Encode(pemRequestBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: derBytes})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding certificate request: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return v.requestVaultCert(commonName, altNames, pemRequestBuf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) initVaultClient() (*vault.Client, error) {
|
||||||
|
client, err := vault.NewClient(nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error initializing Vault client: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetAddress(v.issuer.GetSpec().Vault.Server)
|
||||||
|
|
||||||
|
tokenRef := v.issuer.GetSpec().Vault.Auth.TokenSecretRef
|
||||||
|
if tokenRef.Name != "" {
|
||||||
|
token, err := v.vaultTokenRef(tokenRef.Name, tokenRef.Key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading Vault token from secret %s/%s: %s", v.issuerResourcesNamespace, tokenRef.Name, err.Error())
|
||||||
|
}
|
||||||
|
client.SetToken(token)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
appRole := v.issuer.GetSpec().Vault.Auth.AppRole
|
||||||
|
if appRole.RoleId != "" {
|
||||||
|
token, err := v.requestTokenWithAppRoleRef(client, &appRole)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading Vault token from secret %s/%s: %s", v.issuerResourcesNamespace, appRole.SecretRef.Name, err.Error())
|
||||||
|
}
|
||||||
|
client.SetToken(token)
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("error initializing Vault client. tokenSecretRef or appRoleSecretRef not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) requestTokenWithAppRoleRef(client *vault.Client, appRole *v1alpha1.VaultAppRole) (string, error) {
|
||||||
|
roleId, secretId, err := v.appRoleRef(appRole)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error reading Vault AppRole from secret: %s/%s: %s", appRole.SecretRef.Name, v.issuerResourcesNamespace, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
parameters := map[string]string{
|
||||||
|
"role_id": roleId,
|
||||||
|
"secret_id": secretId,
|
||||||
|
}
|
||||||
|
|
||||||
|
url := "/v1/auth/approle/login"
|
||||||
|
|
||||||
|
request := client.NewRequest("POST", url)
|
||||||
|
|
||||||
|
err = request.SetJSONBody(parameters)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error encoding Vault parameters: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.RawRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error calling Vault server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
vaultResult := vault.Secret{}
|
||||||
|
resp.DecodeJSON(&vaultResult)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to decode JSON payload: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := vaultResult.TokenID()
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("unable to read token: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) requestVaultCert(commonName string, altNames []string, csr string) ([]byte, error) {
|
||||||
|
client, err := v.initVaultClient()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.V(4).Infof("Vault certificate request for commonName %s altNames: %q", commonName, altNames)
|
||||||
|
|
||||||
|
parameters := map[string]string{
|
||||||
|
"common_name": commonName,
|
||||||
|
"alt_names": strings.Join(altNames, ","),
|
||||||
|
"ttl": defaultCertificateDuration.String(),
|
||||||
|
"csr": csr,
|
||||||
|
"exclude_cn_from_sans": "true",
|
||||||
|
}
|
||||||
|
|
||||||
|
url := path.Join("/v1", v.issuer.GetSpec().Vault.Path)
|
||||||
|
|
||||||
|
request := client.NewRequest("POST", url)
|
||||||
|
|
||||||
|
err = request.SetJSONBody(parameters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error encoding Vault parameters: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.RawRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error calling Vault server: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
vaultResult := certutil.Secret{}
|
||||||
|
resp.DecodeJSON(&vaultResult)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode JSON payload: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedBundle, err := certutil.ParsePKIMap(vaultResult.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to parse certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
bundle, err := parsedBundle.ToCertBundle()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to convert certificate bundle to PEM bundle: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte(bundle.ToPEMBundle()), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) appRoleRef(appRole *v1alpha1.VaultAppRole) (roleId, secretId string, err error) {
|
||||||
|
roleId = strings.TrimSpace(appRole.RoleId)
|
||||||
|
|
||||||
|
secret, err := v.secretsLister.Secrets(v.issuerResourcesNamespace).Get(appRole.SecretRef.Name)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
key := "secretId"
|
||||||
|
if appRole.SecretRef.Key != "secretId" {
|
||||||
|
key = appRole.SecretRef.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, ok := secret.Data[key]
|
||||||
|
if !ok {
|
||||||
|
return "", "", fmt.Errorf("no data for %q in secret '%s/%s'", key, appRole.SecretRef.Name, v.issuerResourcesNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretId = string(keyBytes)
|
||||||
|
secretId = strings.TrimSpace(secretId)
|
||||||
|
|
||||||
|
return roleId, secretId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Vault) vaultTokenRef(name, key string) (string, error) {
|
||||||
|
secret, err := v.secretsLister.Secrets(v.issuerResourcesNamespace).Get(name)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if key == "" {
|
||||||
|
key = "token"
|
||||||
|
}
|
||||||
|
|
||||||
|
keyBytes, ok := secret.Data[key]
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("no data for %q in secret '%s/%s'", key, name, v.issuerResourcesNamespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
token := string(keyBytes)
|
||||||
|
token = strings.TrimSpace(token)
|
||||||
|
|
||||||
|
return token, nil
|
||||||
|
}
|
||||||
14
pkg/issuer/vault/prepare.go
Normal file
14
pkg/issuer/vault/prepare.go
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Prepare does nothing for the Vault issuer. In future, this may validate
|
||||||
|
// the certificate request against the issuer, or set fields in the Status
|
||||||
|
// block to be consumed in Issue and Renew
|
||||||
|
func (c *Vault) Prepare(ctx context.Context, crt *v1alpha1.Certificate) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
28
pkg/issuer/vault/renew.go
Normal file
28
pkg/issuer/vault/renew.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errorRenewCert = "ErrRenewCert"
|
||||||
|
messageErrorRenewCert = "Error renewing TLS certificate: "
|
||||||
|
|
||||||
|
successCertRenewed = "CertRenewSuccess"
|
||||||
|
messageCertRenewed = "Certificate renewed successfully"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Vault) Renew(ctx context.Context, crt *v1alpha1.Certificate) ([]byte, []byte, error) {
|
||||||
|
key, cert, err := c.obtainCertificate(ctx, crt)
|
||||||
|
if err != nil {
|
||||||
|
s := messageErrorRenewCert + err.Error()
|
||||||
|
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionFalse, errorRenewCert, s, false)
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
crt.UpdateStatusCondition(v1alpha1.CertificateConditionReady, v1alpha1.ConditionTrue, successCertRenewed, messageCertRenewed, true)
|
||||||
|
|
||||||
|
return key, cert, err
|
||||||
|
}
|
||||||
82
pkg/issuer/vault/setup.go
Normal file
82
pkg/issuer/vault/setup.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
successVaultVerified = "VaultVerified"
|
||||||
|
messageVaultVerified = "Vault verified"
|
||||||
|
|
||||||
|
errorVault = "VaultError"
|
||||||
|
|
||||||
|
messageVaultClientInitFailed = "Failed to initialize Vault client: "
|
||||||
|
messageVaultHealthCheckFailed = "Failed to call Vault health check: "
|
||||||
|
messageVaultStatusVerificationFailed = "Vault is not initialized or is sealed: "
|
||||||
|
messageVaultConfigRequired = "Vault config cannot be empty"
|
||||||
|
messageServerAndPathRequired = "Vault server and path are required fields"
|
||||||
|
messsageAuthFieldsRequired = "Vault tokenSecretRef or appRole is required"
|
||||||
|
messageAuthFieldRequired = "Vault tokenSecretRef and appRole cannot be set on the same issuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (v *Vault) Setup(ctx context.Context) error {
|
||||||
|
if v.issuer.GetSpec().Vault == nil {
|
||||||
|
glog.Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageVaultConfigRequired)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, messageVaultConfigRequired)
|
||||||
|
return fmt.Errorf(messageVaultConfigRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.issuer.GetSpec().Vault.Server == "" ||
|
||||||
|
v.issuer.GetSpec().Vault.Path == "" {
|
||||||
|
glog.Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageServerAndPathRequired)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, messageServerAndPathRequired)
|
||||||
|
return fmt.Errorf(messageVaultConfigRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.issuer.GetSpec().Vault.Auth.TokenSecretRef.Name == "" &&
|
||||||
|
v.issuer.GetSpec().Vault.Auth.AppRole.RoleId == "" &&
|
||||||
|
v.issuer.GetSpec().Vault.Auth.AppRole.SecretRef.Name == "" {
|
||||||
|
glog.Infof("%s: %s", v.issuer.GetObjectMeta().Name, messsageAuthFieldsRequired)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, messsageAuthFieldsRequired)
|
||||||
|
return fmt.Errorf(messsageAuthFieldsRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.issuer.GetSpec().Vault.Auth.TokenSecretRef.Name != "" &&
|
||||||
|
(v.issuer.GetSpec().Vault.Auth.AppRole.RoleId != "" ||
|
||||||
|
v.issuer.GetSpec().Vault.Auth.AppRole.SecretRef.Name != "") {
|
||||||
|
glog.Infof("%s: %s", v.issuer.GetObjectMeta().Name, messageAuthFieldRequired)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, messageAuthFieldRequired)
|
||||||
|
return fmt.Errorf(messageAuthFieldRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := v.initVaultClient()
|
||||||
|
if err != nil {
|
||||||
|
s := messageVaultClientInitFailed + err.Error()
|
||||||
|
glog.V(4).Infof("%s: %s", v.issuer.GetObjectMeta().Name, s)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
health, err := client.Sys().Health()
|
||||||
|
if err != nil {
|
||||||
|
s := messageVaultHealthCheckFailed + err.Error()
|
||||||
|
glog.V(4).Infof("%s: %s", v.issuer.GetObjectMeta().Name, s)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !health.Initialized || health.Sealed {
|
||||||
|
s := messageVaultStatusVerificationFailed + err.Error()
|
||||||
|
glog.V(4).Infof("%s: %s", v.issuer.GetObjectMeta().Name, s)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionFalse, errorVault, s)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
glog.Info(messageVaultVerified)
|
||||||
|
v.issuer.UpdateStatusCondition(v1alpha1.IssuerConditionReady, v1alpha1.ConditionTrue, successVaultVerified, messageVaultVerified)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
64
pkg/issuer/vault/vault.go
Normal file
64
pkg/issuer/vault/vault.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
corelisters "k8s.io/client-go/listers/core/v1"
|
||||||
|
"k8s.io/client-go/tools/record"
|
||||||
|
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
clientset "github.com/jetstack/cert-manager/pkg/client/clientset/versioned"
|
||||||
|
"github.com/jetstack/cert-manager/pkg/issuer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Vault struct {
|
||||||
|
issuer v1alpha1.GenericIssuer
|
||||||
|
|
||||||
|
client kubernetes.Interface
|
||||||
|
cmclient clientset.Interface
|
||||||
|
recorder record.EventRecorder
|
||||||
|
|
||||||
|
secretsLister corelisters.SecretLister
|
||||||
|
|
||||||
|
// issuerResourcesNamespace is a namespace to store resources in. This is
|
||||||
|
// here so we can easily support ClusterIssuers with the same codepath. By
|
||||||
|
// setting this field to either the namespace of the Issuer, or the
|
||||||
|
// clusterResourceNamespace specified on the CLI, we can easily continue
|
||||||
|
// to work with supplemental (e.g. secrets) resources without significant
|
||||||
|
// refactoring.
|
||||||
|
issuerResourcesNamespace string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVault(issuerObj v1alpha1.GenericIssuer,
|
||||||
|
cl kubernetes.Interface,
|
||||||
|
cmclient clientset.Interface,
|
||||||
|
recorder record.EventRecorder,
|
||||||
|
resourceNamespace string,
|
||||||
|
secretsLister corelisters.SecretLister) (issuer.Interface, error) {
|
||||||
|
|
||||||
|
return &Vault{
|
||||||
|
issuer: issuerObj,
|
||||||
|
client: cl,
|
||||||
|
cmclient: cmclient,
|
||||||
|
recorder: recorder,
|
||||||
|
issuerResourcesNamespace: resourceNamespace,
|
||||||
|
secretsLister: secretsLister,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register this Issuer with the issuer factory
|
||||||
|
func init() {
|
||||||
|
issuer.Register(issuer.IssuerVault, func(issuer v1alpha1.GenericIssuer, ctx *issuer.Context) (issuer.Interface, error) {
|
||||||
|
issuerResourcesNamespace := issuer.GetObjectMeta().Namespace
|
||||||
|
if issuerResourcesNamespace == "" {
|
||||||
|
issuerResourcesNamespace = ctx.ClusterResourceNamespace
|
||||||
|
}
|
||||||
|
return NewVault(
|
||||||
|
issuer,
|
||||||
|
ctx.Client,
|
||||||
|
ctx.CMClient,
|
||||||
|
ctx.Recorder,
|
||||||
|
issuerResourcesNamespace,
|
||||||
|
ctx.KubeSharedInformerFactory.Core().V1().Secrets().Lister(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
75
test/e2e/certificate/certificate_vault.go
Normal file
75
test/e2e/certificate/certificate_vault.go
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
package certificate
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
"github.com/jetstack/cert-manager/test/e2e/framework"
|
||||||
|
"github.com/jetstack/cert-manager/test/util"
|
||||||
|
"github.com/jetstack/cert-manager/test/util/vault"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = framework.CertManagerDescribe("Vault Certificate (AppRole)", func() {
|
||||||
|
f := framework.NewDefaultFramework("create-vault-certificate")
|
||||||
|
|
||||||
|
rootMount := "root-ca"
|
||||||
|
intermediateMount := "intermediate-ca"
|
||||||
|
role := "kubernetes-vault"
|
||||||
|
issuerName := "test-vault-issuer"
|
||||||
|
certificateName := "test-vault-certificate"
|
||||||
|
certificateSecretName := "test-vault-certificate"
|
||||||
|
vaultSecretAppRoleName := "vault-role"
|
||||||
|
vaultPath := fmt.Sprintf("%s/sign/%s", intermediateMount, role)
|
||||||
|
var vaultInit *vault.VaultInitializer
|
||||||
|
var roleId string
|
||||||
|
var secretId string
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("Configuring the Vault server")
|
||||||
|
podList, err := f.KubeClientSet.CoreV1().Pods("vault").List(metav1.ListOptions{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
vaultPodName := podList.Items[0].Name
|
||||||
|
vaultInit, err = vault.NewVaultInitializer(vaultPodName, rootMount, intermediateMount, role)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
err = vaultInit.Setup()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
roleId, secretId, err = vaultInit.CreateAppRole()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
_, err = f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(vault.NewVaultAppRoleSecret(vaultSecretAppRoleName, secretId))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
By("Cleaning up")
|
||||||
|
f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Delete(issuerName, nil)
|
||||||
|
f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Delete(vaultSecretAppRoleName, nil)
|
||||||
|
vaultInit.CleanAppRole()
|
||||||
|
vaultInit.Clean()
|
||||||
|
})
|
||||||
|
|
||||||
|
vaultURL := "http://vault.vault:8200"
|
||||||
|
It("should generate a new valid certificate", func() {
|
||||||
|
By("Creating an Issuer")
|
||||||
|
_, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vaultURL, vaultPath, roleId, vaultSecretAppRoleName))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Waiting for Issuer to become Ready")
|
||||||
|
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
|
||||||
|
issuerName,
|
||||||
|
v1alpha1.IssuerCondition{
|
||||||
|
Type: v1alpha1.IssuerConditionReady,
|
||||||
|
Status: v1alpha1.ConditionTrue,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Creating a Certificate")
|
||||||
|
cert, err := f.CertManagerClientSet.CertmanagerV1alpha1().Certificates(f.Namespace.Name).Create(util.NewCertManagerVaultCertificate(certificateName, certificateSecretName, issuerName, v1alpha1.IssuerKind))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
f.WaitCertificateIssuedValid(cert)
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -62,6 +62,9 @@ func RunE2ETests(t *testing.T) {
|
|||||||
extraArgs = append(extraArgs, "--set", "image.tag="+framework.TestContext.PebbleImageTag)
|
extraArgs = append(extraArgs, "--set", "image.tag="+framework.TestContext.PebbleImageTag)
|
||||||
}
|
}
|
||||||
InstallHelmChart(t, "pebble", "./contrib/charts/pebble", "pebble", "./test/fixtures/pebble-values.yaml", extraArgs...)
|
InstallHelmChart(t, "pebble", "./contrib/charts/pebble", "pebble", "./test/fixtures/pebble-values.yaml", extraArgs...)
|
||||||
|
|
||||||
|
InstallHelmChart(t, "vault", "./contrib/charts/vault", "vault", "./test/fixtures/vault-values.yaml")
|
||||||
|
|
||||||
glog.Infof("Starting e2e run %q on Ginkgo node %d", framework.RunId, config.GinkgoConfig.ParallelNode)
|
glog.Infof("Starting e2e run %q on Ginkgo node %d", framework.RunId, config.GinkgoConfig.ParallelNode)
|
||||||
|
|
||||||
var r []ginkgo.Reporter
|
var r []ginkgo.Reporter
|
||||||
|
|||||||
101
test/e2e/issuer/issuer_vault.go
Normal file
101
test/e2e/issuer/issuer_vault.go
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
package issuer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
|
||||||
|
"github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha1"
|
||||||
|
"github.com/jetstack/cert-manager/test/e2e/framework"
|
||||||
|
"github.com/jetstack/cert-manager/test/util"
|
||||||
|
"github.com/jetstack/cert-manager/test/util/vault"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = framework.CertManagerDescribe("Vault Issuer", func() {
|
||||||
|
f := framework.NewDefaultFramework("create-vault-issuer")
|
||||||
|
|
||||||
|
issuerName := "test-vault-issuer"
|
||||||
|
rootMount := "root-ca"
|
||||||
|
intermediateMount := "intermediate-ca"
|
||||||
|
role := "kubernetes-vault"
|
||||||
|
vaultSecretAppRoleName := "vault-role"
|
||||||
|
vaultSecretTokenName := "vault-token"
|
||||||
|
vaultPath := fmt.Sprintf("%s/sign/%s", intermediateMount, role)
|
||||||
|
var roleId, secretId string
|
||||||
|
var vaultInit *vault.VaultInitializer
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
By("Configuring the Vault server")
|
||||||
|
podList, err := f.KubeClientSet.CoreV1().Pods("vault").List(metav1.ListOptions{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
vaultPodName := podList.Items[0].Name
|
||||||
|
vaultInit, err = vault.NewVaultInitializer(vaultPodName, rootMount, intermediateMount, role)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
err = vaultInit.Setup()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
roleId, secretId, err = vaultInit.CreateAppRole()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
By("Cleaning up")
|
||||||
|
f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Delete(issuerName, nil)
|
||||||
|
f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Delete(vaultSecretAppRoleName, nil)
|
||||||
|
vaultInit.CleanAppRole()
|
||||||
|
vaultInit.Clean()
|
||||||
|
})
|
||||||
|
|
||||||
|
const vaultDefaultDuration = time.Hour * 24 * 90
|
||||||
|
|
||||||
|
vaultURL := "http://vault.vault:8200"
|
||||||
|
It("should be ready with a valid AppRole", func() {
|
||||||
|
_, err := f.KubeClientSet.CoreV1().Secrets(f.Namespace.Name).Create(vault.NewVaultAppRoleSecret(vaultSecretAppRoleName, secretId))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Creating an Issuer")
|
||||||
|
_, err = f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vaultURL, vaultPath, roleId, vaultSecretAppRoleName))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Waiting for Issuer to become Ready")
|
||||||
|
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
|
||||||
|
issuerName,
|
||||||
|
v1alpha1.IssuerCondition{
|
||||||
|
Type: v1alpha1.IssuerConditionReady,
|
||||||
|
Status: v1alpha1.ConditionTrue,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail to init with missing Vault AppRole", func() {
|
||||||
|
By("Creating an Issuer")
|
||||||
|
_, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerAppRole(issuerName, vaultURL, vaultPath, roleId, vaultSecretAppRoleName))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Waiting for Issuer to become Ready")
|
||||||
|
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
|
||||||
|
issuerName,
|
||||||
|
v1alpha1.IssuerCondition{
|
||||||
|
Type: v1alpha1.IssuerConditionReady,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail to init with missing Vault Token", func() {
|
||||||
|
By("Creating an Issuer")
|
||||||
|
_, err := f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name).Create(util.NewCertManagerVaultIssuerToken(issuerName, vaultURL, vaultPath, vaultSecretTokenName))
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
By("Waiting for Issuer to become Ready")
|
||||||
|
err = util.WaitForIssuerCondition(f.CertManagerClientSet.CertmanagerV1alpha1().Issuers(f.Namespace.Name),
|
||||||
|
issuerName,
|
||||||
|
v1alpha1.IssuerCondition{
|
||||||
|
Type: v1alpha1.IssuerConditionReady,
|
||||||
|
Status: v1alpha1.ConditionFalse,
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
0
test/fixtures/vault-values.yaml
vendored
Normal file
0
test/fixtures/vault-values.yaml
vendored
Normal file
@ -287,6 +287,22 @@ func NewCertManagerACMECertificate(name, secretName, issuerName string, issuerKi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewCertManagerVaultCertificate(name, secretName, issuerName string, issuerKind string) *v1alpha1.Certificate {
|
||||||
|
return &v1alpha1.Certificate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.CertificateSpec{
|
||||||
|
CommonName: "test.domain.com",
|
||||||
|
SecretName: secretName,
|
||||||
|
IssuerRef: v1alpha1.ObjectReference{
|
||||||
|
Name: issuerName,
|
||||||
|
Kind: issuerKind,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewIngress(name, secretName string, annotations map[string]string, dnsNames ...string) *extv1beta1.Ingress {
|
func NewIngress(name, secretName string, annotations map[string]string, dnsNames ...string) *extv1beta1.Ingress {
|
||||||
return &extv1beta1.Ingress{
|
return &extv1beta1.Ingress{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
@ -360,6 +376,57 @@ func NewCertManagerCAIssuer(name, secretName string) *v1alpha1.Issuer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewCertManagerVaultIssuerToken(name, vaultURL, vaultPath, vaultSecretToken string) *v1alpha1.Issuer {
|
||||||
|
return &v1alpha1.Issuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.IssuerSpec{
|
||||||
|
IssuerConfig: v1alpha1.IssuerConfig{
|
||||||
|
Vault: &v1alpha1.VaultIssuer{
|
||||||
|
Server: vaultURL,
|
||||||
|
Path: vaultPath,
|
||||||
|
Auth: v1alpha1.VaultAuth{
|
||||||
|
TokenSecretRef: v1alpha1.SecretKeySelector{
|
||||||
|
Key: "secretkey",
|
||||||
|
LocalObjectReference: v1alpha1.LocalObjectReference{
|
||||||
|
Name: vaultSecretToken,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCertManagerVaultIssuerAppRole(name, vaultURL, vaultPath, roleId, vaultSecretAppRole string) *v1alpha1.Issuer {
|
||||||
|
return &v1alpha1.Issuer{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Spec: v1alpha1.IssuerSpec{
|
||||||
|
IssuerConfig: v1alpha1.IssuerConfig{
|
||||||
|
Vault: &v1alpha1.VaultIssuer{
|
||||||
|
Server: vaultURL,
|
||||||
|
Path: vaultPath,
|
||||||
|
Auth: v1alpha1.VaultAuth{
|
||||||
|
AppRole: v1alpha1.VaultAppRole{
|
||||||
|
RoleId: roleId,
|
||||||
|
SecretRef: v1alpha1.SecretKeySelector{
|
||||||
|
Key: "secretkey",
|
||||||
|
LocalObjectReference: v1alpha1.LocalObjectReference{
|
||||||
|
Name: vaultSecretAppRole,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSigningKeypairSecret(name string) *v1.Secret {
|
func NewSigningKeypairSecret(name string) *v1.Secret {
|
||||||
return &v1.Secret{
|
return &v1.Secret{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
|||||||
5
test/util/vault/OWNERS
Normal file
5
test/util/vault/OWNERS
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
approvers:
|
||||||
|
- munnerz
|
||||||
|
reviewers:
|
||||||
|
- munnerz
|
||||||
|
- vdesjardins
|
||||||
332
test/util/vault/util.go
Normal file
332
test/util/vault/util.go
Normal file
@ -0,0 +1,332 @@
|
|||||||
|
package vault
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/golang/glog"
|
||||||
|
vault "github.com/hashicorp/vault/api"
|
||||||
|
"k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
const vaultToken = "vault-root-token"
|
||||||
|
|
||||||
|
func NewVaultTokenSecret(name string) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
StringData: map[string]string{
|
||||||
|
"token": vaultToken,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVaultAppRoleSecret(name, secretId string) *v1.Secret {
|
||||||
|
return &v1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
StringData: map[string]string{
|
||||||
|
"secretkey": secretId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type VaultInitializer struct {
|
||||||
|
proxyCmd *exec.Cmd
|
||||||
|
client *vault.Client
|
||||||
|
rootMount string
|
||||||
|
intermediateMount string
|
||||||
|
role string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVaultInitializer(container, rootMount, intermediateMount, role string) (*VaultInitializer, error) {
|
||||||
|
args := []string{"port-forward", "-n", "vault", container, "8200:8200"}
|
||||||
|
cmd := exec.Command("kubectl", args...)
|
||||||
|
err := cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
glog.Fatalf("Error starting port-forward: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
|
||||||
|
cfg := vault.DefaultConfig()
|
||||||
|
cfg.Address = "http://127.0.0.1:8200"
|
||||||
|
client, err := vault.NewClient(cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Unable to initialize vault client: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
client.SetToken(vaultToken)
|
||||||
|
|
||||||
|
return &VaultInitializer{
|
||||||
|
proxyCmd: cmd,
|
||||||
|
client: client,
|
||||||
|
rootMount: rootMount,
|
||||||
|
intermediateMount: intermediateMount,
|
||||||
|
role: role,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) Setup() error {
|
||||||
|
if err := v.mountPKI(v.rootMount, "87600h"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rootCa, err := v.generateRootCert()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.configureCert(v.rootMount); err != nil {
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.mountPKI(v.intermediateMount, "43800h"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
csr, err := v.generateIntermediateSigningReq()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateCa, err := v.signCertificate(csr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.importSignIntermediate(intermediateCa, rootCa, v.intermediateMount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.configureCert(v.intermediateMount); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := v.setupRole(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) Clean() error {
|
||||||
|
if err := v.client.Sys().Unmount("/" + v.intermediateMount); err != nil {
|
||||||
|
return fmt.Errorf("Unable to unmount %v: %v", v.intermediateMount, err)
|
||||||
|
}
|
||||||
|
if err := v.client.Sys().Unmount("/" + v.rootMount); err != nil {
|
||||||
|
return fmt.Errorf("Unable to unmount %v: %v", v.rootMount, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
v.proxyCmd.Process.Kill()
|
||||||
|
v.proxyCmd.Process.Wait()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) CreateAppRole() (string, string, error) {
|
||||||
|
// create policy
|
||||||
|
role_path := path.Join(v.intermediateMount, "sign", v.role)
|
||||||
|
policy := fmt.Sprintf("path \"%s\" { capabilities = [ \"create\", \"update\" ] }", role_path)
|
||||||
|
err := v.client.Sys().PutPolicy(v.role, policy)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Error creating policy: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// # create approle
|
||||||
|
params := map[string]string{
|
||||||
|
"period": "24h",
|
||||||
|
"policies": v.role,
|
||||||
|
}
|
||||||
|
url := path.Join("/v1/auth/approle/role", v.role)
|
||||||
|
_, err = v.callVault("POST", url, "", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Error creating approle: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// # read the role-id
|
||||||
|
url = path.Join("/v1/auth/approle/role", v.role, "role-id")
|
||||||
|
roleId, err := v.callVault("GET", url, "role_id", map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Error reading role_id: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// # read the secret-id
|
||||||
|
url = path.Join("/v1/auth/approle/role", v.role, "secret-id")
|
||||||
|
secretId, err := v.callVault("POST", url, "secret_id", map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("Error reading secret_id: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return roleId, secretId, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) CleanAppRole() error {
|
||||||
|
url := path.Join("/v1/auth/approle/role", v.role)
|
||||||
|
_, err := v.callVault("DELETE", url, "", map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting AppRole: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = v.client.Sys().DeletePolicy(v.role)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error deleting policy: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) mountPKI(mount, ttl string) error {
|
||||||
|
opts := &vault.MountInput{
|
||||||
|
Type: "pki",
|
||||||
|
Config: vault.MountConfigInput{
|
||||||
|
MaxLeaseTTL: "87600h",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if err := v.client.Sys().Mount("/"+mount, opts); err != nil {
|
||||||
|
return fmt.Errorf("Error mounting %s: %s", mount, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) generateRootCert() (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"common_name": "Root CA",
|
||||||
|
"ttl": "87600h",
|
||||||
|
"exclude_cn_from_sans": "true",
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", v.rootMount, "root", "generate", "internal")
|
||||||
|
|
||||||
|
cert, err := v.callVault("POST", url, "certificate", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error generating CA root certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) generateIntermediateSigningReq() (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"common_name": "Intermediate CA",
|
||||||
|
"ttl": "43800h",
|
||||||
|
"exclude_cn_from_sans": "true",
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", v.intermediateMount, "intermediate", "generate", "internal")
|
||||||
|
|
||||||
|
csr, err := v.callVault("POST", url, "csr", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error generating CA intermediate certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return csr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) signCertificate(csr string) (string, error) {
|
||||||
|
params := map[string]string{
|
||||||
|
"use_csr_values": "true",
|
||||||
|
"ttl": "43800h",
|
||||||
|
"exclude_cn_from_sans": "true",
|
||||||
|
"csr": csr,
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", v.rootMount, "root", "sign-intermediate")
|
||||||
|
|
||||||
|
cert, err := v.callVault("POST", url, "certificate", params)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error signing intermediate Vault certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) importSignIntermediate(intermediateCa, rootCa, intermediateMount string) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"certificate": intermediateCa,
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", intermediateMount, "intermediate", "set-signed")
|
||||||
|
|
||||||
|
_, err := v.callVault("POST", url, "", params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error importing intermediate Vault certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) configureCert(mount string) error {
|
||||||
|
params := map[string]string{
|
||||||
|
"issuing_certificates": fmt.Sprintf("http://vault.vault:8200/v1/%s/ca", mount),
|
||||||
|
"crl_distribution_points": fmt.Sprintf("http://vault.vault:8200/v1/%s/crl", mount),
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", mount, "config", "urls")
|
||||||
|
|
||||||
|
_, err := v.callVault("POST", url, "", params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error configuring Vault certificate: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) setupRole() error {
|
||||||
|
// vault auth-enable approle
|
||||||
|
auths, err := v.client.Sys().ListAuth()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error fetching auth mounts: %s", err.Error())
|
||||||
|
}
|
||||||
|
if _, ok := auths["approle/"]; !ok {
|
||||||
|
options := &vault.EnableAuthOptions{Type: "approle"}
|
||||||
|
if err := v.client.Sys().EnableAuthWithOptions("approle", options); err != nil {
|
||||||
|
return fmt.Errorf("Error enabling approle: %s", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// vault write intermediate-ca/roles/kubernetes-vault allow_any_name=true max_ttl="2160h"
|
||||||
|
params := map[string]string{
|
||||||
|
"allow_any_name": "true",
|
||||||
|
"max_ttl": "2160h",
|
||||||
|
}
|
||||||
|
url := path.Join("/v1", v.intermediateMount, "roles", v.role)
|
||||||
|
|
||||||
|
_, err = v.callVault("POST", url, "", params)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error creating role %s: %s", v.role, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *VaultInitializer) callVault(method, url, field string, params map[string]string) (string, error) {
|
||||||
|
req := v.client.NewRequest(method, url)
|
||||||
|
|
||||||
|
err := req.SetJSONBody(params)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error encoding Vault parameters: %s", err.Error())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := v.client.RawRequest(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("error calling Vault server: %s", err.Error())
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
result := map[string]interface{}{}
|
||||||
|
resp.DecodeJSON(&result)
|
||||||
|
|
||||||
|
fieldData := ""
|
||||||
|
if field != "" {
|
||||||
|
data := result["data"].(map[string]interface{})
|
||||||
|
fieldData = data[field].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldData, err
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user