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/issuer/acme"
|
||||
_ "github.com/jetstack/cert-manager/pkg/issuer/ca"
|
||||
_ "github.com/jetstack/cert-manager/pkg/issuer/vault"
|
||||
"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.
|
||||
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
|
||||
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-cert-manager: https://github.com/PalmStoneGames/kube-cert-manager
|
||||
.. _`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
|
||||
simple signing keypair, stored in a Secret
|
||||
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
|
||||
@ -136,6 +138,7 @@ for the ACME issuer, or ``spec.ca`` for the CA based issuer.
|
||||
|
||||
issuers/acme/index
|
||||
issuers/ca/index
|
||||
issuers/vault/index
|
||||
|
||||
.. _`Let's Encrypt`: https://letsencrypt.org
|
||||
.. _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
|
||||
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 {
|
||||
ACME *ACMEIssuer `json:"acme,omitempty"`
|
||||
CA *CAIssuer `json:"ca,omitempty"`
|
||||
ACME *ACMEIssuer `json:"acme,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 {
|
||||
|
||||
@ -728,6 +728,15 @@ func (in *IssuerConfig) DeepCopyInto(out *IssuerConfig) {
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
if in.Vault != nil {
|
||||
in, out := &in.Vault, &out.Vault
|
||||
if *in == nil {
|
||||
*out = nil
|
||||
} else {
|
||||
*out = new(VaultIssuer)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@ -872,3 +881,55 @@ func (in *SecretKeySelector) DeepCopy() *SecretKeySelector {
|
||||
in.DeepCopyInto(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
|
||||
}
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -22,7 +22,8 @@ func (c *Controller) issuersForSecret(secret *corev1.Secret) ([]*v1alpha1.Issuer
|
||||
continue
|
||||
}
|
||||
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)
|
||||
continue
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ const (
|
||||
IssuerACME string = "acme"
|
||||
// IssuerCA is the name of the simple issuer
|
||||
IssuerCA string = "ca"
|
||||
// IssuerVault is the name of the Vault issuer
|
||||
IssuerVault string = "vault"
|
||||
)
|
||||
|
||||
// nameForIssuer determines the name of the issuer implementation given an
|
||||
@ -21,6 +23,8 @@ func nameForIssuer(i v1alpha1.GenericIssuer) (string, error) {
|
||||
return IssuerACME, nil
|
||||
case i.GetSpec().CA != 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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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)
|
||||
|
||||
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 {
|
||||
return &extv1beta1.Ingress{
|
||||
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 {
|
||||
return &v1.Secret{
|
||||
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