8 minute read

Prerequisites

This setup uses Minikube to virtualise a Kubernetes cluster. Minikube is running on a virtual machie using Linux Alpine OS to reduce RAM usage. Additional packages were required (such as docker), but this will not be covered here. It should be noted that Minikube was compiled from source as there is not a particuarly clean way of installing an up-to-date version of it in Alpine (the current package is outdated).

Some additional configuration beyond installing packages was required, which is detailed below.

Enable Br_netfilter

Br_netfilter needs to be enabled to enable bridging on the OS. This is primarily required to allow Kubernetes pods to communicate with other pods using the Kubernetes DNS service. The command to do this is displayed below. The last line confirms that it has been enabled (it would display nothing if disabled).

1
2
3
modprobe br_netfilter
echo "br_netfilter" >> /etc/modules
lsmod | grep br_netfilter

Create offline root CA

An offline root CA is required to sign the Vault intermediate certificate, which will be used to sign certificates for other services on Kubernetes. For this offline root certificate, elliptical curves were chosen due to its smaller key size compared to RSA as specified by NIST. P-521 is generally not accepted in browsers, hence why P-256 was chosen. The command to generate it are detailed below.

openssl genpkey -genparam -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec_parameters

The root certificate and private key are created, which are both valid for 10 years (this is acceptable for a root CA):

openssl req -newkey ec:ec_parameters -noenc -x509 -days 3650 -keyout ca-key.pem -out ca-cert.pem

Generate Vault TLS certificates

The actual connections to Vault need to be encrypted to ensure all transmitted secrets/certificates cannot be intercepted in a man-in-the-middle attack. A seperate TLS certificate is created for this purpose. A certificate signing request is created to do this:

openssl req -newkey ec:ec_parameters -out vault-tls-request.csr -keyout vault-tls-key.pem -config vault-tls-config.conf -noenc

The configuration file is found below. The main thing to note is the DNS names which correspond to various Vault services running under the Vault namespace (which is where Vault is running).

[req]
prompt = no
distinguished_name = kubelet_serving
req_extensions = v3_req
[ kubelet_serving ]
O = system:nodes
CN = system:node:*.vault.svc.cluster.local
[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.vault-internal
DNS.2 = *.vault-internal.vault.svc.cluster.local
DNS.3 = *.vault
IP.1 = 127.0.0.1

This request is then signed using the root CA and is made valid for 3 years. The copy extensions flag is important as it ensures the certificate signing request above is followed.

openssl x509 -req -in vault-tls-request.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out vault-tls-cert.pem -days 1095 -copy_extensions copyall

This TLS certificate is then combined with the root certificate to form a chain of verification:

cat vault-tls-cert.pem ca-cert.pem > vault-tls-bundle.pem

Some secrets are created within Kubernetes to provide Vault with the certificates created above:

kubectl create secret generic vault-ca -n vault --from-file=vault.ca=ca-cert.pem

kubectl create secret tls vault-tls -n vault --cert vault-tls-bundle.pem --key vault-tls-key.pem

Configure Helm chart for Vault

Helm will be utilised to install Vault. The configuration below utilises the secrets above to start Vault. The configuration can be applied by running the following:

helm install -n vault vault hashicorp/vault -f helm-config.yaml

The contents of the helm-config.yaml are provided below:

global:
  enabled: true
  namespace: "vault"
  tlsDisable: false

server:
  enabled: true
  image:
    # Set image details to control version upgrades
    repository: "hashicorp/vault"
    tag: "2.0.4"
  logLevel: "info"
  logFormat: "json"
  resources:
    requests:
      memory: 2Gi
      cpu: 2000m
    limits:
      memory: 4Gi
      cpu: 2000m
  extraEnvironmentVars:
    # Used to verify the CA certificate when using the command line
    VAULT_CACERT: /vault/userconfig/vault-ca/vault.ca
  volumes:
    # Attaches the Root CA certificate
    - name: userconfig-vault-ca
      secret:
        defaultMode: 420
        secretName: vault-ca
    # Attaches the Vault TLS certificate and private key
    - name: userconfig-vault-tls
      secret:
        defaultMode: 420
        secretName: vault-tls
  # Mounts the volumes above
  volumeMounts:
    - mountPath: /vault/userconfig/vault-ca
      name: userconfig-vault-ca
      readOnly: true
    - mountPath: /vault/userconfig/vault-tls
      name: userconfig-vault-tls
      readOnly: true
  standalone:
    enabled: false
  ha:
    enabled: true
    replicas: 1 # Increasing this requires the 'raft' storage config to be further configured
    raft:
      enabled: true
      setNodeId: true
      config: |
        cluster_name = "vault-integrated-storage"
        disable_mlock = true
        service_registration "kubernetes" {}

        ui = true

        storage "raft" {
          path = "/vault/data"
        }

        listener "tcp" {
          address = "[::]:8200"
          cluster_address = "[::]:8201"
          tls_disable = "false" 
          tls_cert_file = "/vault/userconfig/vault-tls/tls.crt"
          tls_key_file = "/vault/userconfig/vault-tls/tls.key"
          tls_client_ca_file = "/vault/userconfig/vault-ca/vault.ca"
        }

Configuring Vault

Initialising Vault

The command below can be used to configure the vault instance with 1 key share. This is not suitable for production, but is acceptable for this testing use case.

kubectl exec -n vault vault-0 -- vault operator init -key-shares=1 -key-threshold=1 -format=json > cluster-keys.json

Accessing the UI

To make configuration more accessible, the vault service is port forwarded and binded to 0.0.0.0, which allows the Vault UI to be accessed from the host machine (as it is running within a VM).

kubectl -n vault port-forward service/vault 8200:8200 --address 0.0.0.0

Create a PKI secret

Here I created a PKI secret called pki-int. I set the mount and AIA path as the following:

Showcases the cluster mount path

And set the global URLs as the following as well:

Showcases the cluster URL paths

Sign Vault intermediate certificate

To configure certificate signing on Vault, we first need to assign Vault an intermediate certificate from our offline root CA. This certificate signing request is then utilised in the following command (the number of days matches the days specified in a previous command):

openssl x509 -req -in vault-int-signing-request.csr -CA ca-cert.pem -CAkey ca-key.pem -CAcreateserial -out vault-int-cert.pem -days 1095 -copy_extensions copyall

Bundle this intermediate cert with the root cert to create a chain of verification:

cat vault-int-cert.pem ca-cert.pem > vault-int-bundle.pem

Set this bundle as the TLS certificate in Vault. This creates two issuers, one for the intermediate and another for the submitted root CA certificate (the private key is not provided for this root CA so it cannot be used to sign any certificates on Vault).

Showcases the certificate issuers avaliable on Vault

Configuring Mongodb

The MongoDB Kubernetes operator was installed to configure MongoDB databases on Kubernetes. The resource definition below was utilised to create these instances. Ideally the number of replica members should be set to 2 or more for redundancy, but due to a lack of memory/CPU, only 1 was utilised for testing purposes. certificateKeySecretRef and caCertificateSecretRef refer to a future secret created by cert-manager containing the certificates issued by Vault to MongoDB.

apiVersion: mongodbcommunity.mongodb.com/v1
kind: MongoDBCommunity
metadata:
  name: example-mongodb
spec:
  members: 1
  type: ReplicaSet
  version: "6.0.5"
  security:
    authentication:
      modes: ["SCRAM"]
    tls:
      enabled: true
      certificateKeySecretRef:
        name: tls-certificate
      caCertificateSecretRef:
        name: tls-certificate
  users:
    - name: my-user
      db: admin
      passwordSecretRef:
        name: my-user-password
      roles:
        - name: clusterAdmin
          db: admin
        - name: userAdminAnyDatabase
          db: admin
      scramCredentialsSecretName: my-scram
---
apiVersion: v1
kind: Secret
metadata:
  name: my-user-password
type: Opaque
stringData:
  password: <password here>

Creating an issuing role in Vault

An issuing role is created on Vault which is used to issue certificates to the MongoDB service. The goal of this issuer is to issue short term certificates (31 days validity) which are then auto-renewed by cert-manager in Kubernetes. The following domain settings were configured, along with setting an elliptical curve key for a smaller key size agaisnt RSA:

Showcases the Vault issuer domain configurations for issuing certificates to the MondoDB database

Showcases the Vault issuer key configuration for issuing certificates to the MondoDB database

Configuring authorisation policy

An authorisation policy is required to ensure an authenticated cert-manager service can retrive the certificate created by Vault:

Showcases the authorisation policy for retrieving the MongoDB certificates on Vault

Configuring authentication method

Cert-manager needs to be able to authenticate itself to Vault in order to retrieve the MongoDB certificates. This is done by running the commands below (these are performed on the command line instead of the web UI as the last command utilises an environment variable).

1
2
3
4
kubectl exec --stdin=true --tty=true vault-0 -n vault -- /bin/sh
vault login <auth token here>
vault auth enable kubernetes
vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"

The following authentication role is then created within the Kubernetes authentication method and the token’s initial TTL is set to 10 minutes (short-lived). The policy we created above (mongodb-issuer-policy) is also assigned to this role to configure its access to least priviledge permissions:

Showcases the Kubernetes policy for retrieving the MongoDB certificates on Vault

Configure cert-manager

Cert-manager can now be installed to be configured with the role created above. This requires a service account be be created within the Kubernetes MongoDB namespace (notice that the service account name below matches the name defined above):

kubectl create serviceaccount mongodb-issuer -n mongodb

A service account token is then created for this role, which is used to authenticate to Vault:

apiVersion: v1
kind: Secret
metadata:
  name: mongodb-issuer-token
  annotations:
    kubernetes.io/service-account.name: mongodb-issuer
type: kubernetes.io/service-account-token

An issuer in cert-manager is then created which authenticates to Vault and retrieves certificates from it based on the certificate issuing role in Vault. Note that the server pointed to is https://vault.vault:8200, this refers to the vault service running within the vault namespace.

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
  name: mongodb-vault-issuer
  namespace: mongodb
spec:
  vault:
    server: https://vault.vault:8200
    path: pki-int/sign/mongodb-role
    caBundle: <insert Vault intermediate certificate bundle here as base64>
    auth:
      kubernetes:
        mountPath: /v1/auth/kubernetes
        role: mongodb-issuer-role
        secretRef:
          name: mongodb-issuer-token
          key: token

After this is created, the certificate itself is then created as a resource. This saves the CA certificate, TLS certificate and TLS key within a secret called tls-certificate (as previously mentioned)

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: mongodb-tls
  namespace: mongodb
spec:
  secretName: tls-certificate
  issuerRef:
    name: mongodb-vault-issuer
  # Below needs to be specified as the requested certificate uses an EC key
  privateKey:
    algorithm: ECDSA
    size: 256
  commonName: "*.example-mongodb-svc.mongodb.svc.cluster.local"
  dnsNames:
  - "*.example-mongodb-svc.mongodb.svc.cluster.local"

The certificate can be seen being created successfully as seen below:

Showcases the MongoDB certificate being successfully issued

Confirming TLS is enabled

After the TLS certificate has been issued, the MongoDB instances need to be restarted, which can be done by running the following command:

kubectl rollout restart statefulset example-mongodb -n mongodb

Once the pods have all restarted, we can then log into the MongoDB instance using TLS:

kubectl exec -it example-mongodb-0 -c mongod -n mongodb -- bash

Then connecting to the instance (ensuring the root CA certificate is specified as trusted) results in:

Showcases MongoDB successfully being connected to using TLS

This showcases a successful connection to the MongoDB database using TLS, which was configured via certificates retrived from Vault using cert-manager.