Let's Encrypt DNS-01 (WIP)

Last modified by Tomas Terälä on 2025/06/16 17:35

If using a HTTP-challenge is not an option, it is possible to use DNS-challenges.

Information

This instuction assumes a certain level of technical ability and the permissions to create network registrations at the University of Helsinki.

TL;DR

POST to https://acme-server.it.helsinki.fi/register from the internal network, create a DNS-record where the cname is the full domain + '.' from the HTTP-response and the name is _acme-challenge.domain (without .helsinki.fi). Then create a Secret in Kubernetes that has the information from the HTTP-response in json-form, then refer to that in your issuer.

Long form

This instruction will split off at the end, depending on if you want to do TLS-termination yourself or if the certificate is used by a OpenShift Route. But the start is the same for both

  1. Get credentials
  2. Create a DNS-entry

Outside Kubernetes

Credentials

The credentials can be self provisioned by posting to https://acme-server.it.helsinki.fi/register from the university's internal network.

curl -X POST https://acme-server.it.helsinki.fi/register

#response edited for clarity, later referred to as "HTTP_RESPONSE"
{
"username":"0774057d-7a72-4f97-ba90-d54466c401de",
"password":"<redacted>",
"fulldomain":"50c47584-51b5-42bc-b33f-565421af03ad.acme.it.helsinki.fi",
"subdomain":"50c47584-51b5-42bc-b33f-565421af03ad",
"allowfrom":[]
}

DNS-entry

Create DNS entry, where the CNAME is the fulldomain from the earlier HTTP_RESPONSE and the FQDN is the name starting with _acme-challenge. It might take up to 15 minutes for the DNS-entry to populate to all servers.

#verify that the record exists
dig +short @8.8.8.8 _acme-challenge.acme-dns.web.helsinki.fi

50c47584-51b5-42bc-b33f-565421af03ad.acme.it.helsinki.fi.

Kubernetes

  1. Create a file called acmedns.json from the contents of HTTP_RESPONSE
  2. Create a Secret from the acmedns.json file
  3. Create an Issuer that references the Secret
  4. Diverging paths:
    1. Ingress/Route
      1. (Create DNS-record for your FQDN)
      2. Create Ingress
    2. Handling TLS-termination
      1. (Create DNS-record for your FQDN)
      2. Create a Certificate
      3. Create a passthrough Route
      4. Mount the Secret that the Certificate creates in your Pod
Information

All examples use Let's Encrypt's staging environment, after making sure that everything works check the main ACME page for instructions on using the production instance of Let's Encrypt.

All examples for the FQDN: acme-dns.web.helsinki.fi

Creating the Secret

# Create acmedns.json
echo '{ "acme-dns.web.helsinki.fi": {HTTP_RESPONSE}' > acmedns.json

# Create a Secret from acmedns.json
oc create secret generic <secret-name> -n <namespace> --from-file=acmedns.json=acmedns.json
Information

Make sure that acmedns.json only has a single set of braces around the HTTP_RESPONSE, e.g

Good: ...fi: {"username"...,"allowfrom":[]}}'}
Bad: ...fi: {{"username"...,"allowfrom":[]}}'} 

Create Issuer

The normal info about Issuer/Certificate/CertificateRequest objects apply, as seen here:

apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
 name: letsencrypt-staging
 namespace: <namespace>
spec:
 acme:
   email: <email>
   privateKeySecretRef:
     name: letsencrypt-staging-acme-key
   server: https://acme-staging-v02.api.letsencrypt.org/directory
   solvers:
    - dns01:
       acmeDNS:
         accountSecretRef:
           key: acmedns.json
           name: <secret-name>
         host: https://acme-server.it.helsinki.fi

Ingress/Route

Create an Ingress that mentions the Issuer

kind: Ingress
apiVersion: networking.k8s.io/v1
metadata:
 name: acme-dns-web-helsinki-fi-staging
 namespace: <namespace>
 annotations:
   cert-manager.io/issuer: letsencrypt-staging
spec:
 ingressClassName: openshift-default
 tls:
    - hosts:
        - <FQDN>
     secretName: FQN-cert-staging #change periods "." into dashes "-"
 rules:
    - host: <FQDN>
     http:
       paths:
          - path: /
           pathType: Prefix
           backend:
             service:
               name: <service-name>
               port:
                 number: <port>

After a while, you can check that the Certificate and CertificateRequest objects are fine, e.g. If you created a CNAME for your FQDN, you can also try accessing it using a browser. Note that sometimes clearing cookies or using incognito mode is required to see the new certificate in a browser.

oc get certificate -n namespace

NAME                                        READY   SECRET                            AGE
acme-dns-web-helsinki-fi-cert               True    acme-dns-web-helsinki-fi-cert     19m

oc get certificaterequest -n namespace

NAME                                          APPROVED   DENIED   READY   ISSUER                             REQUESTER                                         AGE
acme-dns-web-helsinki-fi-cert-1               True                True    letsencrypt-staging                system:serviceaccount:cert-manager:cert-manager   19m

Handling TLS-termination

If you want to create a passthrough type Route and need the TLS secret for your application, a Certificate object is required

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
 name: acme-dns-web-helsinki-fi-staging
 namespace: <namespace>
spec:
 dnsNames:
  - acme-dns.web.helsinki.fi
 issuerRef:
   group: cert-manager.io
   kind: Issuer
   name: letsencrypt-staging
 secretName: acme-dns-web-helsinki-fi-staging-cert
 usages:
  - digital signature
  - key encipherment

You can then create a Passthrough Route and then have your Pod mount the Secret defined in spec.secretName.