Set up automatic cert renewal for your self-hosted services using acme.sh. In the examples below I use the porkbun api and acme.sh to issue and renew letsencrypt certificates.
Prerequisites
RBAC settings for our CronJob, so the k8s Secret containing the certs can be created and updated automatically. A persistentVolumeClaim is also created so the files generated by acme.sh in the CronJob can be persisted.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: acme-sh-pvc
namespace: mynamespace
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: acme-sh-sa
namespace: mynamespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: acme-sh-role
namespace: mynamespace
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: acme-sh-rolebinding
namespace: mynamespace
subjects:
- kind: ServiceAccount
name: acme-sh-sa
namespace: mynamespace
roleRef:
kind: Role
name: acme-sh-role
apiGroup: rbac.authorization.k8s.io
CronJob
Uses the above configured ServiceAccount. The script will check for existing certificates and creates them if there aren’t any yet. Existing certificates will be renewed if necessary. The certificate chain and the private key will be stored in a k8s Secret.
apiVersion: batch/v1
kind: CronJob
metadata:
name: acme-sh-renew
namespace: mynamespace
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
serviceAccountName: acme-sh-sa
restartPolicy: OnFailure
securityContext:
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
runAsNonRoot: true
containers:
- name: acme-sh
image: ghcr.io/acmesh-official/acme.sh@sha256:34d0c9a75e0f5222b9bab885cbb3c0c01ae4414f4bebbc16bdbc2298140970ef
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
envFrom:
- secretRef:
name: porkbun-api-keys
env:
- name: DOMAIN
value: "home.mydomain.com"
volumeMounts:
- name: acme-data
mountPath: /acme.sh
command: ["/bin/sh", "-c"]
args:
- |
if [ ! -f /acme.sh/kubectl ]; then
echo "Downloading kubectl"
curl -sSL -o /acme.sh/kubectl "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x /acme.sh/kubectl
else
echo "kubectl already installed"
fi
/acme.sh/kubectl version
create_secret() {
/acme.sh/kubectl create secret tls tls-secret \
--namespace=mynamespace \
--cert=/acme.sh/${DOMAIN}_ecc/fullchain.cer \
--key=/acme.sh/${DOMAIN}_ecc/${DOMAIN}.key \
--dry-run=client \
-o yaml \
| /acme.sh/kubectl apply -f -
}
echo "Make sure acme.sh auto-upgrade is disabled"
sed -i "s/AUTO_UPGRADE='1'/AUTO_UPGRADE='0'/g" /acme.sh/account.conf
if [ ! -f /acme.sh/${DOMAIN}_ecc/fullchain.cer ]; then
echo "Issuing new cert"
acme.sh --issue --server letsencrypt --dns dns_porkbun -d "$DOMAIN" -d "*.${DOMAIN}"
create_secret
else
echo "Execute renewal"
acme.sh --cron --renew-hook "create_secret"
fi
volumes:
- name: acme-data
persistentVolumeClaim:
claimName: acme-sh-pvc
Gateway and HTTPRoutes
The Gateway uses the Secret generated by the CronJob. HTTPRoutes will use the Gateway to terminate TLS. With the config below, HTTP traffic will be redirected to HTTPS.
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: gateway
namespace: mynamespace
spec:
gatewayClassName: traefik
listeners:
- name: web
protocol: HTTP
port: 8000
hostname: "*.home.mydomain.com"
allowedRoutes:
namespaces:
from: All
- name: websecure
protocol: HTTPS
port: 8443
hostname: "*.home.mydomain.com"
tls:
mode: Terminate
certificateRefs:
- name: tls-secret
allowedRoutes:
namespaces:
from: All
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myservice-redirect-route
namespace: mynamespace
spec:
hostnames:
- "myservice.home.mydomain.com"
parentRefs:
- name: gateway
sectionName: web
namespace: mynamespace
rules:
- filters:
- type: RequestRedirect
requestRedirect:
scheme: https
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myservice-https-route
namespace: mynamespace
spec:
parentRefs:
- name: gateway
sectionName: websecure
namespace: mynamespace
hostnames:
- "myservice.home.mydomain.com"
rules:
- backendRefs:
- name: myservice
port: 8080