Vault issuer support

vault remove duration
This commit is contained in:
Vincent Desjardins 2018-05-02 00:33:50 +00:00
parent 230f59b0ac
commit b35343786e
32 changed files with 1459 additions and 5 deletions

View File

@ -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"
) )

View File

@ -0,0 +1,4 @@
apiVersion: v1
description: A Helm chart for Kubernetes
name: vault
version: 0.1.0

View 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 -}}

View 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

View File

@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: vault
labels:
app: vault
spec:
ports:
- name: vault
port: 8200
selector:
app: vault

View File

@ -0,0 +1,4 @@
image:
repository: vault
tag: "0.9.3"
pullPolicy: IfNotPresent

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
approvers:
- munnerz
reviewers:
- munnerz
- vdesjardins

View 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

View File

@ -10,3 +10,4 @@ cert-manager.
acme/index acme/index
ca/index ca/index
vault/index

View File

@ -0,0 +1,5 @@
approvers:
- munnerz
reviewers:
- munnerz
- vdesjardins

View 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

View 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

View File

@ -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 {

View File

@ -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
}

View File

@ -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
} }

View File

@ -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
} }

View File

@ -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
View File

@ -0,0 +1,5 @@
approvers:
- munnerz
reviewers:
- munnerz
- vdesjardins

280
pkg/issuer/vault/issue.go Normal file
View 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
}

View 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
View 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
View 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
View 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(),
)
})
}

View 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)
})
})

View File

@ -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

View 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
View File

View 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
View File

@ -0,0 +1,5 @@
approvers:
- munnerz
reviewers:
- munnerz
- vdesjardins

332
test/util/vault/util.go Normal file
View 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
}