Skip to content

Instantly share code, notes, and snippets.

@amcginlay
Last active March 8, 2023 13:07
Show Gist options
  • Save amcginlay/6cf8c2f89c8f25f0a2e106f69cf5494a to your computer and use it in GitHub Desktop.
Save amcginlay/6cf8c2f89c8f25f0a2e106f69cf5494a to your computer and use it in GitHub Desktop.

TLSPK Venafi Enhanced Issuer with TLSPC and Vault

Terminology:

  • TLSPK: TLS Protect for Kubernetes (previously Jetstack Secure or JSS)
  • TLSPC: TLS Protect Cloud (previously Venafi as a Service or VaaS)
  • TLSP: TLS Protect Data Centre (previously Venafi Trust Protection Platform or TPP)
  • VEI: Venafi Enhanced Issuer (not to be confused with the native cert-manager issuer for Venafi)

cert-manager's native Venafi issuer requires Kubernetes secrets to hold Venafi credentials (e.g. API keys). Ideally you wish to eliminate the use of all secrets as these create a potential attack vector.

With the help of TLSPK js-operator, your goal is to use Venafi credentials stored off-cluster to request certificates from either TLSP or TLSPC.

In this is example your certificate issuance platform will be TLSPC and your credentials will be stored in Hashicorp Vault.

NOTE: the version of VenafiEnhancedIssuer currently enabled with js-operator (jsctl v0.1.17) pre-dates VenafiConnection, which is why that CRD is missing and, for now, you need the separate helm install.

Until further notice, do not attempt to kubectl edit installation.

Monitor deployments

It is generally good practice to monitor your Kubernetes objects, ensuring your dependencies are in place (i.e. Running) at the point of need

watch "kubectl get pods,crds -A | grep -v system"

Create KinD cluster

k8s_name=kind-$(date +"%y%m%d%H%M")
cat <<EOF | kind create cluster --config -
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: ${k8s_name}
nodes:
  - role: control-plane
EOF

Bootstrap TLSPK

# rm -rf ~/.jsctl/ # (optional)
jsctl auth login
jsctl config set organization gallant-wright # (change org as appropriate)
jsctl registry auth init

Install js-operator

kubectl create namespace jetstack-secure

jsctl operator deploy --auto-registry-credentials
sleep 2 && kubectl -n jetstack-secure wait --for=condition=Available=True --all deployments --timeout=-1s

sleep 5 && jsctl operator installations apply --auto-registry-credentials --cert-manager-replicas 1
sleep 2 && kubectl -n jetstack-secure wait --for=condition=Available=True --all deployments --timeout=-1s

Install VEI controller

helm -n jetstack-secure upgrade -i vei \
  oci://eu.gcr.io/jetstack-secure-enterprise/charts/venafi-enhanced-issuer \
  --version v0.3.2 \
  --registry-config <(jsctl registry auth output --format=dockerconfig) \
  --set 'global.imagePullSecrets[0].name=jse-gcr-creds' \
  --wait

At this point you may choose to update your watch command to monitor new CRD type objects, for example

watch "kubectl get pods,venaficonnections,venaficlusterissuers,certificaterequestpolicies -A | grep -v system"

Create Kubernetes Service Account (for Vault)

The VenafiConnection object you are about to create will use this service account to establish communication with Vault. Observe how this references serviceaccounts/token.

cat <<EOF | kubectl -n jetstack-secure apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tlspc-sa
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: create-tokens-for-tlspc-issuer
rules:
  - apiGroups: [""]
    resources: ["serviceaccounts/token"]
    verbs: ["create"]
    resourceNames: ["tlspc-sa"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tlspc-issuer-sa-rolebinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: create-tokens-for-tlspc-issuer
subjects:
  - kind: ServiceAccount
    name: venafi-connection
    namespace: jetstack-secure
EOF

Install Vault inside cluster

helm repo add hashicorp https://helm.releases.hashicorp.com && helm repo update
helm -n vault upgrade -i vault \
  hashicorp/vault \
  --create-namespace \
  --set "server.dev.enabled=true" \
  --wait
sleep 5 && kubectl -n vault wait -l statefulset.kubernetes.io/pod-name=vault-0 --for=condition=ready pod --timeout=-1s

Enable Vault as a credentials provider for the cluster

Note this references tlspc-sa created previously.

kubectl exec -n vault pods/vault-0 -- \
    vault auth enable -path=tlspc kubernetes

kubectl exec -n vault pods/vault-0 -- \
    vault write auth/tlspc/config kubernetes_host=https://kubernetes.default.svc

kubectl exec -n vault pods/vault-0 -- \
    vault write auth/tlspc/role/tlspc-role \
       role_type=jwt \
       bound_audiences=vault.vault.svc.cluster.local \
       user_claim=sub \
       bound_service_account_names=tlspc-sa \
       bound_service_account_namespaces=jetstack-secure \
       policies=tlspc-policy-readonly \
       ttl=5m

cat <<EOF | kubectl exec -i -n vault pods/vault-0 -- vault policy write tlspc-policy-readonly -
path "secret/data/tlspc-connection/creds" {
  capabilities = ["read"]
}
EOF

Store the TLSPC credentials in Vault

api_key=<TLSPC_API_KEY> # <- get this from https://ui.venafi.cloud/platform-settings/user-preferences

kubectl exec -n vault pods/vault-0 -- vault kv put -mount=secret tlspc-connection/creds api-key=${api_key}

Create your VenafiConnection object (reuseable)

Note how this uses tlspc-sa to pull the TLSPC API key out of Vault.

cat <<EOF | kubectl -n jetstack-secure apply -f -
apiVersion: jetstack.io/v1alpha1
kind: VenafiConnection
metadata:
  name: tlspc-connection
spec:
  vaas:
    apiKey:
      - serviceAccountToken:
          name: tlspc-sa
          audiences: [vault.vault.svc.cluster.local]
      - hashicorpVaultOAuth:
          authInputType: OIDC
          role: tlspc-role
          authPath: /v1/auth/tlspc/login
          url: http://vault.vault.svc.cluster.local:8200
      - hashicorpVaultSecret:
          secretPath: /v1/secret/data/tlspc-connection/creds
          fields: ["api-key"]
          url: http://vault.vault.svc.cluster.local:8200
EOF

Create your VEI object

Create the issuer object which references the VenafiConnection object you just created. Observe how this depends upon the Default\Default zone. Please adjust to suit.

When creating the object directly like this, no matching CertificateRequestPolicy object is created. You will address this shortly.

cat <<EOF | kubectl apply -f -
apiVersion: jetstack.io/v1alpha1
kind: VenafiClusterIssuer
metadata:
  name: tlspc-issuer
spec:
  venafiConnectionName: tlspc-connection
  zone: Default\Default
EOF

Create a cluster-wide "open" issuer policy.

When cert-manager is installed via TLSPK it comes with the policy-approver component. When you create an issuer via the operator (or its installation manifest) it automatically OPENs the policy to allow ALL certs to pass through. This case does not apply here.

As you created your venafi-enhanced-issuer object by hand you do not get automatic policy provision and the policy-approver will block ALL cert requests until you configure an appropriate policy, also by hand. The following policy (and RBAC) allows certs to be defined in terms of ANY commonName and ANY dnsNames which is enough to cover the certs you require today.

cat << EOF | kubectl apply -f -
apiVersion: policy.cert-manager.io/v1alpha1
kind: CertificateRequestPolicy
metadata:
  name: certificate-request-policy-tlspc
spec:
  allowed:
    commonName:
      value: '*'
    dnsNames:
      values:
      - '*'
  selector:
    issuerRef:
      group: jetstack.io
      kind: VenafiClusterIssuer
      name: tlspc-issuer
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: certificate-request-policy-tlspc:rbac
rules:
  - apiGroups: ["policy.cert-manager.io"]
    resources: ["certificaterequestpolicies"]
    verbs: ["use"]
    resourceNames: ["certificate-request-policy-tlspc"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: certificate-request-policy-tlspc:rbac
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: certificate-request-policy-tlspc:rbac
subjects:
- kind: Group
  name: system:authenticated
  apiGroup: rbac.authorization.k8s.io
EOF

Create a certificate

We can test the above works by requesting a certificate.

kubectl create namespace tests
cat << EOF | kubectl -n tests apply -f -
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: www.vei-demo.com
spec:
  secretName: www-vei-demo-com-tls
  commonName: www.vei-demo.com
  dnsNames:
    - www.vei-demo.com
  issuerRef:
    name: tlspc-issuer
    kind: VenafiClusterIssuer
    group: jetstack.io
EOF

Confirm certificate issuance

Check the CertificateRequest is "Ready".

kubectl -n tests get cr

If it is, inspect the secret.

kubectl -n tests get secret www-vei-demo-com-tls -o 'go-template={{index .data "tls.crt" | base64decode}}' | openssl x509 -noout -text | head -11

Terminate all

kind delete cluster --name ${k8s_name}

Appendix

If you wish to connect your cluster to TLSPK (optional), you may do so as follows

k8s_name_tlspk=$(tr "-" "_" <<< ${k8s_name})
jsctl clusters connect ${k8s_name_tlspk}
# jsctl clusters delete ${k8s_name_tlspk} --force
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment