For the complete documentation index, see llms.txt. This page is also available as Markdown.

Using Custom Certificates

Starting from this release, you can configure DU-specific SSL/TLS certificates for your Self-hosted Private Cloud Director deployment, issued and automatically renewed by cert-manager. Previously, the system only used a single self-signed wildcard certificate generated during the deployment process.

Overview

By default, the system generates a self-signed wildcard certificate during installation and shares it across every DU namespace. You can now:

  • Configure cert-manager to issue a certificate per DU namespace (for example, via Let's Encrypt)

  • Revert back to the automatically generated self-signed wildcard certificate at any time

Platform9 does not generate or accept a certificate/key file directly (there is no "bring your own .crt/.key" upload flow) — cert-manager, configured with an issuer of your choice, is what actually issues the certificate. airctl only orchestrates the switch: it validates your issuer, requests a certificate per DU namespace, and manages the internal replication labels so the certificate isn't silently reverted to the self-signed wildcard on the next label sync.

Prerequisites

Before running airctl provision-certs, cert-manager must already be installed in the cluster with a working issuer — provision-certs does not install cert-manager or create the issuer for you.

Requirement
How to confirm

cert-manager is installed in the cluster

kubectl get pods -n cert-manager

A ClusterIssuer exists and is Ready

kubectl get clusterissuer <name>

A DNS-01 solver is configured on the issuer if the node has no publicly reachable port 80

kubectl get svc -n ingress-nginx -o wide — if only 443/444/10254 are exposed, ACME HTTP-01 cannot work and the issuer must use a DNS-01 solver (for example, Route53) instead

The example below sets up a ClusterIssuer using Let's Encrypt with a Route53 DNS-01 solver — the common choice for nodes without inbound port 80 reachability. Any Ready cert-manager ClusterIssuer works; the solver and ACME server are entirely up to you.

1

Create the Route53 credentials secret

Only the AWS secret access key is treated as sensitive — the access key ID is a plain field on the ClusterIssuer spec.

kubectl create secret generic route53-credentials-secret -n cert-manager \
  --from-literal=secret-access-key='<AWS_SECRET_ACCESS_KEY>' \
  --dry-run=client -o yaml | kubectl apply -f -
2

Create the ClusterIssuer (staging first)

Validate against Let's Encrypt's staging environment before spending a production rate-limit slot.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: <cluster-issuer-name>
spec:
  acme:
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    email: <your-email>
    privateKeySecretRef:
      name: <cluster-issuer-name>-account-key
    solvers:
      - dns01:
          route53:
            region: <aws-region>
            hostedZoneID: <hosted-zone-id>
            accessKeyID: <aws-access-key-id>
            secretAccessKeySecretRef:
              name: route53-credentials-secret
              key: secret-access-key
kubectl apply -f clusterissuer.yaml
kubectl get clusterissuer <cluster-issuer-name>

READY: True here only confirms the ACME account registered — it needs a syntactically valid email and a private key, nothing more. It does not prove the AWS credentials or Route53 permissions are correct; that's only exercised when a certificate actually triggers a DNS-01 challenge.

3

Validate with a throwaway certificate

Before running provision-certs, confirm the DNS-01 → Route53 → issuance pipeline works end-to-end with a disposable certificate that doesn't share a secret name with anything real:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: route53-dns01-test
  namespace: <any-namespace>
spec:
  commonName: dns01test.<your-domain>
  dnsNames:
    - dns01test.<your-domain>
  issuerRef:
    name: <cluster-issuer-name>
    kind: ClusterIssuer
  secretName: route53-dns01-test-tls
kubectl apply -f test-cert.yaml
kubectl get certificate route53-dns01-test -n <any-namespace> -w

Issuance typically takes a couple of minutes end to end — the ACME challenge usually reports valid well before the Certificate itself flips to Ready; give it time past that point before assuming it's stuck. Clean up once confirmed:

kubectl delete certificate route53-dns01-test -n <any-namespace>
kubectl delete secret route53-dns01-test-tls -n <any-namespace>

Configure DU-Specific Certificates via cert-manager

Once you have a Ready ClusterIssuer, hand it to airctl to provision certificates for every DU namespace:

This command:

  • Validates that <cluster-issuer-name> exists and is Ready — it fails fast, before touching any namespace, if the issuer is missing or not ready.

  • Removes the internal cert-manager-tls replication label from each DU namespace, so the shared self-signed wildcard certificate is not re-synced over the new certificate.

  • Creates (or updates) a cert-manager Certificate resource in every DU namespace, requesting <namespace>.<domain> from the issuer, and waits for it to become Ready.

  • Restarts any deployment in the namespace that mounts the certificate secret, so the new certificate is picked up immediately.

  • Persists clusterIssuerName to /opt/pf9/airctl/conf/airctl-config.yaml, so subsequent airctl upgrade runs know this namespace's certificate is managed by cert-manager and skip overwriting it with the self-signed wildcard cert.

Renewal after this point is handled entirely by cert-manager — no further manual steps are needed.

Switch from staging to production

Once staging issuance is proven end-to-end, re-apply the ClusterIssuer with acme.server pointed at the production endpoint (https://acme-v02.api.letsencrypt.org/directory) — always a full apply, never a partial patch (see the solvers warning above). cert-manager does not proactively reissue an already-Ready certificate just because the issuer's server field changed, so re-run:

Let's Encrypt's production environment enforces a rate limit of 5 duplicate certificates per exact domain set per 7 days. Confirm staging issuance is fully working before switching.

Verify the certificate is actually being served

A Ready certificate only means cert-manager wrote the secret — confirm the ingress is serving it, and confirm real trust (not curl -k, which skips verification and proves nothing):

Staging certificates only verify successfully on a host that explicitly trusts the Let's Encrypt staging root — that's expected and not a sign anything is broken.

Reverting to the Self-Signed Certificate

To restore the automatically generated self-signed wildcard certificate at any time, run:

This command:

  • Deletes the cert-manager Certificate resource for each DU namespace.

  • Restores the original self-signed certificate and key directly into each namespace's certificate secret.

  • Re-adds the cert-manager-tls replication label to each namespace, so it resumes receiving the shared self-signed wildcard certificate.

  • Restarts any deployment that mounts the certificate secret.

  • Clears clusterIssuerName from /opt/pf9/airctl/conf/airctl-config.yaml.

Important Notes

  • Ensure that your DU_FQDN environment variable or the duFqdn field in the airctl configuration file matches the domain specified in your ClusterIssuer-issued certificates.

  • The user running provision-certs must have access to a Ready ClusterIssuer in the cluster.

  • --cluster-issuer and --revert cannot be combined.

  • You can switch between cert-manager-issued and self-signed certificates at any time using airctl provision-certs.

Last updated

Was this helpful?