This article shows how to use a private ipa-server to provide certificates to kubernetes applications.

There is a very good post on the subject about how to configure Identity Manager (ipa-server in RHEL) by Josep Font. A developer subscription for RHEL at no cost can be used, or CentOS Stream can be used for playing with the latest ipa-server version.

Another really good post about Cert-manager integration is done by another two colleagues, Jose Angel de Bustos and Jorge Tudela. They configure the HTTP-01 challenge using an Ingress Controller to expose the web needed for it. Although it is a very straightforward alternative, I do like to use the DNS-01 challenge best.

Note: For more information about ACME protocol and its challenges, see the original RFC8555 Let’s Encrypt ACME documentation.

Installation

First, we need an ipa server installed with dns server and acme configured. It can be done in two simple steps (on top of CentOS or registered RHEL):

  • Package installation:
dnf install ipa-server ipa-server-dns bind-dyndb-ldap
  • Installing ipa-server:
ipa-server-install --unattended --realm=<MYDOMAIN.COM> --domain=<mydomain.com> \
      --ds-password=<db_pass> --admin-password=<admin_pass> \
      --setup-dns --forwarder=<upstream_dns_server> --auto-reverse
  • Configuring ACME:
ipa-acme-manage enable
  • Checking if ACME is enabled:
ipa-acme-manage status

Adding credentials for DNS-01

The IPA server with kerberos comes with TSIG (Transaction SIGnatures) for authenticating DNS updates. However, it is not very usual that programs use Kerberos for those tasks, so we will use another algorithm. In our case, hmac-sha512 is selected.

Tip: For more background on TSIG and its use in DNS updates, see the ISC BIND documentation.

Fortunately, there is a command that creates the key for us.

tsig-keygen -a hmac-sha512 update | tee -a /etc/named/ipa-ext.conf

The key is stored in an extension config file with name update. The update key file will be needed afterwards to test the server. And now we update the named configuration, and check that the new key is reloaded.

rndc reconfig
rndc tsig-list |grep bind |grep update

Should return something like

view "_bind"; type "static"; key "update";

Now, the update policy for the zone has to be updated, but for using ipa command, first a kinit command should get the kerberos ticket.

Note: If you are new to Kerberos, you can find more information in the MIT Kerberos documentation.

echo <admin_pass> | kinit admin
export DNSZONE_UPDATE_POLICY=$(ipa dnszone-show <mydomain.com> --all | awk '/BIND update policy:/ { $1=""; print substr($0,2) }')
ipa dnszone-mod <mydomain.com> --update-policy="${DNSZONE_UPDATE_POLICY}; grant update subdomain <mydomain.com>.;"

Now we can test the acme server to get new certificates

Client example with acme.sh

Acme.sh is a very popular tool used to get certificates using ACME protocol.

More info: The acme.sh wiki contains many examples and advanced usage scenarios.

To test it, first the key file should be stored in the client:

tail -4 /etc/named/ipa-ext.conf > /tmp/update.key
export NSUPDATE_KEY=/tmp/update.key

Complete the NSUPDATE_SERVER with the ip of ipa server and the ACME_SERVER with https://<ipa-server>:8443/acme/directory

Then a new account should be created with the :

acme.sh --register-account --accountkeylength 2048 --force

With a result similar than:

[Mon Jun  2 08:01:17 PM CEST 2025] Registering account: https://<ipa-server>:8443/acme/directory
[Mon Jun  2 08:01:18 PM CEST 2025] Registered

And now, a new cert is required:

acme.sh --issue --dns dns_nsupdate -d <newrecord> --insecure --dnssleep 10 --keylength 2048

The insecure flag is only required for quick testing, in a prod environment CA flags should be used.

Something like the following should be output:

[Wed Jun  4 09:29:58 PM CEST 2025] Using CA: https://<ipa-server>:8443/acme/directory
[Wed Jun  4 09:29:58 PM CEST 2025] Single domain='<newrecord>'
[Wed Jun  4 09:29:58 PM CEST 2025] Getting webroot for domain='<newrecord>'
[Wed Jun  4 09:29:58 PM CEST 2025] Adding TXT value: 1QH7pi9TiSxWM65DfgEZ4EZ1Ei0Irl4ihxAzyrzU-J0 for domain: _acme-challenge.<newrecord>
[Wed Jun  4 09:29:59 PM CEST 2025] adding _acme-challenge.<newrecord>. 60 in txt "1QH7pi9TiSxWM65DfgEZ4EZ1Ei0Irl4ihxAzyrzU-J0"
[Wed Jun  4 09:29:59 PM CEST 2025] The TXT record has been successfully added.
[Wed Jun  4 09:29:59 PM CEST 2025] Sleeping for 10 seconds to wait for the the TXT records to take effect
[Wed Jun  4 09:30:10 PM CEST 2025] Verifying: <newrecord>
[Wed Jun  4 09:30:10 PM CEST 2025] Processing. The CA is processing your order, please wait. (1/30)
[Wed Jun  4 09:30:13 PM CEST 2025] Success
[Wed Jun  4 09:30:13 PM CEST 2025] Removing DNS records.
[Wed Jun  4 09:30:13 PM CEST 2025] Removing txt: 1QH7pi9TiSxWM65DfgEZ4EZ1Ei0Irl4ihxAzyrzU-J0 for domain: _acme-challenge.<newrecord>
[Wed Jun  4 09:30:13 PM CEST 2025] removing _acme-challenge.<newrecord>. txt
[Wed Jun  4 09:30:13 PM CEST 2025] Successfully removed
[Wed Jun  4 09:30:13 PM CEST 2025] Verification finished, beginning signing.
[Wed Jun  4 09:30:13 PM CEST 2025] Let's finalize the order.
[Wed Jun  4 09:30:13 PM CEST 2025] Le_OrderFinalize='https://<ipa-server>/acme/v1/order/c6KsIdqfVr/finalize'
[Wed Jun  4 09:30:14 PM CEST 2025] Downloading cert.
[Wed Jun  4 09:30:14 PM CEST 2025] Le_LinkCert='https://<ipa-server>/acme/v1/cert/Tr-txFLHPAWkRigeHD_Taw'
[Wed Jun  4 09:30:14 PM CEST 2025] Cert success.
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
[Wed Jun  4 09:30:14 PM CEST 2025] Your cert is in: /home/rgordill/.acme.sh/<newrecord>/<newrecord>.cer
[Wed Jun  4 09:30:14 PM CEST 2025] Your cert key is in: /home/rgordill/.acme.sh/<newrecord>/<newrecord>.key
[Wed Jun  4 09:30:14 PM CEST 2025] The intermediate CA cert is in: /home/rgordill/.acme.sh/<newrecord>/ca.cer
[Wed Jun  4 09:30:14 PM CEST 2025] And the full-chain cert is in: /home/rgordill/.acme.sh/<newrecord>/fullchain.cer

Client example with cert manager

Cert Manager is a very popular operator in Kubernetes to provide certificates to workloads and ingress controllers.

Reference: See the cert-manager DNS01 documentation for more details on DNS challenge configuration.

To enable ACME with DNS01, we only need to configure an issuer (in our case, a cluster issuer) with the same params as we have used in acme.sh. The update key will be stored in a secret, but instead of the full file as we did in acme.sh, we need just the key value.

metadata:
  name: tsig-secret
  namespace: cert-manager
type: Opaque
stringData: 
  tsig-secret-key: <secret-key>

The other information (algorithm, key name, etc) would be under rfc2136 element:

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: acme-issuer
  namespace: cert-manager
spec:
  acme:
    email: admin@<domain>
    server: https://<ipa-server>:8443/acme/directory
    privateKeySecretRef:
      name: acme-issuer-key
    skipTLSVerify: true
    solvers:
    - dns01:
        rfc2136:
          nameserver: <ipa-server>:53
          tsigKeyName: update
          tsigAlgorithm: HMACSHA512
          tsigSecretSecretRef:
            name: tsig-secret
            key: tsig-secret-key

When querying the object status, if everything is correct, something like this should exist.

...
status:
  acme:
    lastPrivateKeyHash: 1FmC/C2A4SYazMtmVG0MaaizFF4ZPeOGhLxRSAsKv90=
    lastRegisteredEmail: admin@<domain>
    uri: https://<ipa-server>:8443/acme/v1/acct/Tcm2uttrfYXyrNxtDFIFE4HK9C8r6lkvVF60eKt_NTc
  conditions:
  - lastTransitionTime: "2025-06-05T06:14:55Z"
    message: The ACME account was registered with the ACME server
    observedGeneration: 1
    reason: ACMEAccountRegistered
    status: "True"
    type: Ready

Then, we can try to get a sample certificate.

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: www01
  namespace: cert-manager
spec:
  secretName: www01
  duration: 2160h # 90d
  renewBefore: 360h # 15d
  subject:
    organizations:
      - Org Name
  commonName: '<newrecord>'
  privateKey:
    algorithm: RSA
    encoding: PKCS1
    size: 2048
    rotationPolicy: Always
  usages:
    - server auth
    - client auth
  dnsNames:
    - <newrecord>
  issuerRef:
    name: acme-issuer
    kind: ClusterIssuer

And again, if everything is fine, something like the following would be in status:

Troubleshooting: If the certificate is not issued, check the cert-manager logs and ensure the DNS records are being updated as expected. The cert-manager troubleshooting guide can help diagnose common issues.

...
status:
  conditions:
  - lastTransitionTime: "2025-06-05T06:26:38Z"
    message: Certificate is up to date and has not expired
    observedGeneration: 1
    reason: Ready
    status: "True"
    type: Ready
  notAfter: "2025-09-03T06:26:38Z"
  notBefore: "2025-06-05T06:26:38Z"
  renewalTime: "2025-08-19T06:26:38Z"
  revision: 1

And a new secret would be in that namespace

kubectl get secret -n cert-manager www01 -o yaml
apiVersion: v1
data:
  tls.crt: <redacted>
  tls.key: <redacted>
kind: Secret
metadata:
  annotations:
    cert-manager.io/alt-names: <newrecord>
    cert-manager.io/certificate-name: www01
    cert-manager.io/common-name: <newrecord>
    cert-manager.io/ip-sans: ""
    cert-manager.io/issuer-group: ""
    cert-manager.io/issuer-kind: ClusterIssuer
    cert-manager.io/issuer-name: acme-issuer
    cert-manager.io/uri-sans: ""
  creationTimestamp: "2025-06-05T06:26:38Z"
  labels:
    controller.cert-manager.io/fao: "true"
  name: www01
  namespace: cert-manager
  resourceVersion: "3626"
  uid: ba434928-1167-403d-8ab5-72387ad6ce76
type: kubernetes.io/tls