sachua / sachua.github.io

https://sachua.github.io/
0 stars 0 forks source link

Highly Available Intermediate Certificate Authority Using Kubernetes #6

Open sachua opened 8 months ago

sachua commented 8 months ago

In our own private infrastructure environment, we often need to use our own self-signed TLS certificates to serve our sites over HTTPS.

Step CA can help you generate TLS certificates for your sites using the ACME protocol, and automate the TLS certificate renewal process as well.

In this post we will walk though the process of deploying a PostgreSQL cluster on Kubernetes, then deploying a Step CA Intermediate Certificate Authority that will use the PostgreSQL cluster as the database.

Here is how the final architecture will look like:

Architecture

## Preparation To start the entire deployment, let's create the `stepca` Kubernetes namespace where everything will be deployed to: ```bash kubectl create ns stepca ``` Sign a leaf TLS cert for the domain where you will be hosting your Step CA. We can inject the TLS secret as follows: ```bash kubectl create secret tls stepca-tls -n stepca --key private.key --cert public.crt ``` Download the `step` binary [here](https://github.com/smallstep/cli/releases) and place the binary in `/usr/bin` Then generate an intermediate certificate signing request: ```bash step certificate create "Intermediate CA Name" intermediate.csr intermediate_ca_key --csr ``` Transfer the certificate signing request to your existing root CA and get it signed. You should have the `root_ca.crt` from your existing root CA, `intermediate_ca.crt` from signing the certificate signing request, and `intermediate_ca.key` that was created when you generated the intermediate certificate signing request from the previous step. ## Deploying a PostgreSQL Cluster There are many available PostgreSQL Operators that can help you lifecycle a highly available PostgreSQL cluster. In this example, we will be using CloudNativePG. I like CloudNativePG because the operator creates the database instances using `Pods` instead of `StatefulSets`, which avoids all the limitations that comes with `StatefulSets`. To install the CloudNativePG Operator, run the following command: ```bash kubectl apply -f \ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.22/releases/cnpg-1.22.0.yaml ``` You can verify the Operator is installed with: ```bash kubectl get deployment -n cnpg-system cnpg-controller-manager ``` Here is the YAML deployment file to create the Postgres Cluster. In this example we will be storing our postgres backups to our own MinIO instance hosted at `minio.domain.org` ```YAML # Inject MinIO Credentials as Secret apiVersion: v1 kind: Secret metadata: name: minio namespace: stepca type: Opaque stringData: ACCESS_KEY_ID: minio # S3 username ACCESS_SECRET_KEY: minio123 # S3 password --- # Step CA Deployment YAML apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: stepca-postgres namespace: stepca spec: imageName: ghcr.io/cloudnative-pg/postgresql:15.0 instances: 3 primaryUpdateStrategy: unsupervised # Rolling update process to be automated and managed by Kubernetes monitoring: enablePodMonitor: true # Expose prometheus metrics storage: storageClass: vsan-default-storage-policy # Define the storage class you use in your Kubernetes cluster size: 1Gi backup: # Configure to use S3 to store backup resources barmanObjectStore: destinationPath: s3://stepca/ # S3 bucket location endpointURL: https://minio.domain.org # S3 endpoint s3Credentials: accessKeyId: name: minio key: ACCESS_KEY_ID secretAccessKey: name: minio key: ACCESS_SECRET_KEY wal: compression: gzip encryption: AES256 retentionPolicy: "7d" ``` You should see the following Kubernetes resources once your PostgreSQL cluster is created: Services: - stepca-postgres-r - applications to connect to any of the instances for read-only workloads - stepca-postgres-ro - applications to connect to any of the hot standby, non-primary replicas for read-only workloads - stepca-postgres-rw - applications to connect to the primary instance for read-write workloads Secrets: - stepca-postgres-app - database credentials for the default user called `app`, corresponds to the user owning the database - stepca-postgres-ca - self-signed CA generated and used to support TLS within the postgres cluster - stepca-postgres-replication - streaming replication client certificate generated by the client CA - stepca-postgres-server - server TLS certificate signed by the server CA - stepca-postgres-superuser - superuser credentials to be used only for administrative purposes, corresponds to the `postgres` user - stepca-postgres-token - kubernetes service account created for the database operator Monitoring: - When enablePodMonitor is set to `true`, CloudNativePG will automatically expose prometheus metrics relating to CloudNativePG clusters, and create a `PodMonitor` resource for your prometheus to scrape the endpoint - The pre-requisite is that you must have prometheus already installed ### PostgreSQL Backups For on-demand backups, apply the following `YAML`: ``` apiVersion: postgresql.cnpg.io/v1 kind: Backup metadata: name: backup-example spec: cluster: name: stepca-postgres ``` To schedule backups, apply the following `YAML`: ```YAML apiVersion: postgresql.cnpg.io/v1 kind: ScheduledBackup metadata: name: backup-daily-midnight namespace: stepca spec: schedule: "0 0 16 * * *" # 0000 SGT in UTC time backupOwnerReference: self cluster: name: stepca-postgres ``` To restore backup from S3 object store, apply the following `YAML`: ```YAML apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: stepca-postgres namespace: stepca spec: imageName: ghcr.io/cloudnative-pg/postgresql:15.0 instances: 3 primaryUpdateStrategy: unsupervised # Rolling update process to be automated and managed by Kubernetes monitoring: enablePodMonitor: true # Expose prometheus metrics storage: storageClass: vsan-default-storage-policy # Define the storage class you use in your Kubernetes cluster size: 1Gi backup: # Configure to use S3 to store backup resources barmanObjectStore: destinationPath: s3://stepca/ # S3 bucket location endpointURL: https://minio.domain.org # S3 endpoint s3Credentials: accessKeyId: name: minio key: ACCESS_KEY_ID secretAccessKey: name: minio key: ACCESS_SECRET_KEY wal: compression: gzip encryption: AES256 retentionPolicy: "7d" bootstrap: recovery: source: stepca-postgres recoveryTarget: # Use backupID and targetImmediate to backup to that instant and stop immediately, or use targetTime to do point-in-time recovery where the database will run through the WAL to the timestamp specified after restoring from the nearest base backup backupID: 20240102T160000 targetTime: "2024-01-02T09:00:25" externalClusters: - name: stepca-postgres # Name has to be same as the previous cluster since barman will be searching for backups based on the name barmanObjectStore: destinationPath: s3://stepca/ endpointURL: https://minio.domain.org # S3 endpoint s3Credentials: accessKeyId: name: minio key: ACCESS_KEY_ID secretAccessKey: name: minio key: ACCESS_SECRET_KEY wal: maxParallel: 8 # Take advantage of the parallel WAL restore feature to dedicate up to 8 concurrent jobs to fetch required WAL files from the archive ``` ## Deploying Step CA Step CA includes a helm chart to deploy on Kubernetes, but there is a disclaimer that they can only support 1 replica instance. Therefore we will not be using their helm chart, and instead we take the following deployment path: 1. Run a Step CA on Docker 2. Retrieve the configuration template to use in our actual deployment 3. Deploy our Step CA with our specified configuration injected as a Kubernetes secret To run Step CA on Docker, we can use the following `docker-compose.yml` file: ```YAML version: '3.3' services: ca: image: smallstep/step-ca:0.24.1 networks: - default ports: - "9000:9000" environment: - DOCKER_STEPCA_INIT_NAME=${DOCKER_STEPCA_INIT_NAME} # Name of your CA - this will be the issuer of your CA certificates - DOCKER_STEPCA_INIT_DNS_NAMES=${DOCKER_STEPCA_INIT_DNS_NAMES} # Hostname(s) or IPs that the CA will accept requests on - DOCKER_STEPCA_INIT_PROVISIONER_NAME=${DOCKER_STEPCA_INIT_PROVISIONER_NAME} # Label for the initial admin (JWK) provisioner. Default: "admin" - DOCKER_STEPCA_INIT_PASSWORD=${DOCKER_STEPCA_INIT_PASSWORD} # Password for the encrypted CA keys and the default CA provisioner volumes: - ./data/home/step:/home/step restart: always networks: default: ipam: driver: default config: - subnet: "172.97.0.0/16" volumes: step_home: ``` From here, we can retrieve the configuration files from `/home/step/`, then edit the template to use the PostgreSQL cluster that we deployed earlier. A reference of the configuration options can be found [here](https://smallstep.com/docs/step-ca/configuration/#basic-configuration-options) A customised `config/ca.json` is as follows: ```JSON { "root": "/home/step/certs/root_ca.crt", "federatedRoots": null, "crt": "/home/step/certs/intermediate_ca.crt", "key": "/home/step/certs/intermediate_ca.key", "address": "9000", "insecureAddress": "true", "dnsNames": [ "localhost", "ca.domain.org" ], "logger": { "format": "text" }, "db":{ "type": "postgresql", "dataSource": "postgresql://app:password@stepca-postgres-rw.stepca.svc.cluster.local:5432", "database": "app" }, "authority": { "provisioners": [ { "type": "JWK", "name": "admin", "key": { "use": "sig", "kty": "EC", "kid": "YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs", "crv": "P-256", "alg": "ES256", "x": "LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg", "y": "F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w" }, "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q", "claims": { "enableSSHCA": false, "disableRenewal": false, "allowRenewalAfterExpiry": false }, "options": { "x509": {}, "ssh": {} } }, { "type": "ACME", "name": "acme", "forceCN": true, "claims": { "maxTLSCertDuration": "2160h0m0s", "defaultTLSCertDuration": "2160h0m0s", "policy": { "x509": { "allow": ["*.domain.org"] } } }, "options": { "x509": { "templateFile": "templates/certs/x509/leaf.tpl" } } } ], "template": {}, "backdate": "1m0s" }, "tls": { "cipherSuites": [ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ], "minVersion": 1.2, "maxVersion": 1.3, "renegotiation": false }, "commonName": "Step Online CA" } ``` This `ca.json` references a custom leaf cert template `leaf.tpl` to set Subject Alternative Name (SAN) in the provisioned TLS certificate. Check [here](https://smallstep.com/docs/step-ca/templates/) on how to configure your own Step CA templates. The custom `leaf.tpl` as follows: ``` { "subject": {{ toJson .subject }}, {{- if .Insecure.User.dnsName }} "dnsNames": {{ toJSON .Insecure.User.dnsName }}, {{- else }} "sans": {{ toJson .SANs }}, {{- end }} {{-if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }} "keyUsage": ["keyEncipherment", "digitalSignature"], {{- else }} "keyUsage": ["digitalSignature"], {{- end }} "extKeyUsage": ["serverAuth", "clientAuth] } ``` We can then create the entire deployment YAML file including the injecting of configuration as secrets, and the referencing of the secrets from the deployment resource. Our example deployment assumes an NGINX ingress controller is already deployed in the cluster. The `deployment.yaml` is as follows: ```YAML apiVersion: v1 kind: Secret metadata: name: stepca-config namespace: stepca type: Opaque stringData: intermediate_ca_crt: | root_ca.crt: | ca.json: | { "root": "/home/step/certs/root_ca.crt", "federatedRoots": null, "crt": "/home/step/certs/intermediate_ca.crt", "key": "/home/step/certs/intermediate_ca.key", "address": "9000", "insecureAddress": "true", "dnsNames": [ "localhost", "ca.domain.org" ], "logger": { "format": "text" }, "db":{ "type": "postgresql", "dataSource": "postgresql://app:password@stepca-postgres-rw.stepca.svc.cluster.local:5432", "database": "app" }, "authority": { "provisioners": [ { "type": "JWK", "name": "admin", "key": { "use": "sig", "kty": "EC", "kid": "YYNxZ0rq0WsT2MlqLCWvgme3jszkmt99KjoGEJJwAKs", "crv": "P-256", "alg": "ES256", "x": "LsI8nHBflc-mrCbRqhl8d3hSl5sYuSM1AbXBmRfznyg", "y": "F99LoOvi7z-ZkumsgoHIhodP8q9brXe4bhF3szK-c_w" }, "encryptedKey": "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJjdHkiOiJqd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjEwMDAwMCwicDJzIjoiVERQS2dzcEItTUR4ZDJxTGo0VlpwdyJ9.2_j0cZgTm2eFkZ-hrtr1hBIvLxN0w3TZhbX0Jrrq7vBMaywhgFcGTA.mCasZCbZJ-JT7vjA.bW052WDKSf_ueEXq1dyxLq0n3qXWRO-LXr7OzBLdUKWKSBGQrzqS5KJWqdUCPoMIHTqpwYvm-iD6uFlcxKBYxnsAG_hoq_V3icvvwNQQSd_q7Thxr2_KtPIDJWNuX1t5qXp11hkgb-8d5HO93CmN7xNDG89pzSUepT6RYXOZ483mP5fre9qzkfnrjx3oPROCnf3SnIVUvqk7fwfXuniNsg3NrNqncHYUQNReiq3e9I1R60w0ZQTvIReY7-zfiq7iPgVqmu5I7XGgFK4iBv0L7UOEora65b4hRWeLxg5t7OCfUqrS9yxAk8FdjFb9sEfjopWViPRepB0dYPH8dVI.fb6-7XWqp0j6CR9Li0NI-Q", "claims": { "enableSSHCA": false, "disableRenewal": false, "allowRenewalAfterExpiry": false }, "options": { "x509": {}, "ssh": {} } }, { "type": "ACME", "name": "acme", "forceCN": true, "claims": { "maxTLSCertDuration": "2160h0m0s", "defaultTLSCertDuration": "2160h0m0s", "policy": { "x509": { "allow": ["*.domain.org"] } } }, "options": { "x509": { "templateFile": "templates/certs/x509/leaf.tpl" } } } ], "template": {}, "backdate": "1m0s" }, "tls": { "cipherSuites": [ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" ], "minVersion": 1.2, "maxVersion": 1.3, "renegotiation": false }, "commonName": "Step Online CA" } defaults.json: | { "ca-url": "https://localhost:9000", "ca-config": "/home/step/config/ca.json", "fingerprint": "93cff06dc36251fb0c4985d0b5ed7265a368cd70697fba90355c93cc4aabff0d", "root": "/home/step/certs/root_ca.crt" } intermediate_ca_key: | password: | leaf.tpl: | { "subject": {{ toJson .subject }}, {{- if .Insecure.User.dnsName }} "dnsNames": {{ toJSON .Insecure.User.dnsName }}, {{- else }} "sans": {{ toJson .SANs }}, {{- end }} {{-if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }} "keyUsage": ["keyEncipherment", "digitalSignature"], {{- else }} "keyUsage": ["digitalSignature"], {{- end }} "extKeyUsage": ["serverAuth", "clientAuth] } --- apiVersion: apps/v1 kind: Deployment metadata: name: stepca namespace: stepca spec: replicas: 1 selector: matchLabels: app: stepca template: metadata: labels: app: stepca spec: containers: - name: stepca image: smallstep/step-ca:0.23.0 ports: - containerPort: 9000 env: - name: DOCKER_STEPCA_INIT_NAME value: stepca - name: DOCKER_STEPCA_INIT_DNS_NAMES value: localhost, ca.domain.org - name: DOCKER_STEPCA_INIT_PROVISIONER_NAME value: admin - name: DOCKER_STEP_CA_INIT_PASSWORD valueFrom: secretKeyRef: name: stepca-config key: password volumeMounts: - mountPath: /home/step name: stepca-config readOnly: false volumes: - name: stepca-config secret: secretName: stepca-config items: - key: intermediate_ca.crt path: certs/intermediate_ca.crt - key: root_ca.crt path: certs/root_ca.crt - key: ca.json path: config/ca.json - key: defaults.json path: config/defaults.json - key: intermediate_ca_key path: secrets/intermediate_ca_key - key: password path: secrets/password - key: leaf.tpl path: templates/certs/x509/leaf.tpl defaultMode: 0755 --- apiVersion: v1 kind: Service metadata: name: stepca namespace: stepca labels: app: stepca spec: type: ClusterIP ports: - port: 9000 selector: app: stepca --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: stepca-ingress namespace: stepca annotations: kubernetes.io/ingress.class: 'nginx' nginx.ingress.kubernetes.io/backend-protocol: 'HTTPS' spec: tls: - hosts: - ca.domain.org secretName: stepca-tls rules: - host: ca.domain.org http: paths: - path: / pathType: Prefix backend: service: name: stepca port: number: 9000 --- apiVersion: autoscaling/v1 kind: HorizontalPodAutoscaler metadata: name: stepca namespace: stepca labels: app: stepca resource: horizontalpodautoscaler spec: maxReplicas: 6 minReplicas: 1 scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: stepca targetCPUUtilizationPercentage: 80 ``` You check the health of your Intermediate Certificate Authority: ```bash curl https://ca.domain.org/health # {"status":"ok"} curl https://ca.domain.org/acme/acme/directory # {"newNonce":"https://ca.domain.org/acme/acme/new-nonce","newAccount":"https://ca.domain.org/acme/acme/new-account","newOrder":"https://ca.domain.org/acme/acme/new-order","revokeCert":"https://ca.domain.org/acme/acme/revoke-cert","keyChange":"https://ca.domain.org/acme/acme/key-change"} ```