ISE Guest Portal Certificate Automation
Overview
Automate Let’s Encrypt certificate renewal for ISE Guest/Sponsor Portal using:
-
certbot - ACME client for Let’s Encrypt
-
Cloudflare DNS challenge - Domain validation without exposing ports
-
k3s CronJob - Scheduled automation
-
ISE ERS API - Certificate import
| Component | Value |
|---|---|
Guest Portal FQDN |
guest.domusdigitalis.dev |
Certificate Authority |
Let’s Encrypt (publicly trusted) |
Automation Platform |
k3s CronJob |
DNS Provider |
Cloudflare |
Renewal Frequency |
Every 60 days (certs valid 90 days) |
Architecture
┌─────────────────┐ DNS-01 challenge ┌─────────────────┐
│ k3s CronJob │ ◀─────────────────────▶ │ Let's Encrypt │
│ (certbot) │ │ ACME Server │
└────────┬────────┘ └─────────────────┘
│
│ 1. Get cert via Cloudflare DNS challenge
│ 2. Push to ISE via ERS API
▼
┌─────────────────┐
│ ISE │
│ Guest Portal │
│ (Portal cert) │
└─────────────────┘
Prerequisites
Cloudflare API Token
# Create token with Zone:DNS:Edit permissions
# Store in gopass
gopass insert v3/domains/d000/services/cloudflare/dns-edit-token
# Or add to dsec
dsec edit d000 dev/app
# Add: CF_DNS_API_TOKEN=<token>
ISE ERS API Credentials
# Verify ERS is enabled
# ISE Admin → Administration → System → Settings → API Settings → ERS
# Enable ERS for Read/Write
# Credentials in dsec
dsource d000 dev/network
# $ISE_USER, $ISE_PASS should be available
k8s Namespace and Secrets
# Create namespace
kubectl create namespace cert-automation
# Create Cloudflare credentials secret
kubectl create secret generic cloudflare-dns \
--namespace cert-automation \
--from-literal=api-token=$(gopass show -o v3/domains/d000/services/cloudflare/dns-edit-token)
# Create ISE credentials secret
kubectl create secret generic ise-api \
--namespace cert-automation \
--from-literal=username=$ISE_USER \
--from-literal=password=$ISE_PASS
Phase 1: k3s CronJob Manifest
1.1 ConfigMap for Scripts
apiVersion: v1
kind: ConfigMap
metadata:
name: cert-automation-scripts
namespace: cert-automation
data:
renew-and-push.sh: |
#!/bin/bash
set -euo pipefail
DOMAIN="guest.domusdigitalis.dev"
ISE_HOST="ise-01.inside.domusdigitalis.dev"
echo "=== Renewing certificate for $DOMAIN ==="
# Run certbot with DNS challenge
certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/cloudflare/credentials.ini \
--non-interactive \
--agree-tos \
--email admin@domusdigitalis.dev \
-d "$DOMAIN" \
--cert-path /certs
echo "=== Pushing certificate to ISE ==="
# Convert to PKCS12 for ISE import
openssl pkcs12 -export \
-in /etc/letsencrypt/live/$DOMAIN/fullchain.pem \
-inkey /etc/letsencrypt/live/$DOMAIN/privkey.pem \
-out /tmp/guest-portal.p12 \
-passout pass:$PKCS12_PASSWORD
# Import to ISE via ERS API
# TODO: Implement netapi ise cert-import or use curl
curl -k -X POST \
-u "$ISE_USER:$ISE_PASS" \
-H "Content-Type: application/json" \
"https://$ISE_HOST/api/v1/certs/system-certificate/import" \
-d @- <<EOF
{
"admin": false,
"eap": false,
"portal": true,
"pxgrid": false,
"allowWildcardCerts": false,
"data": "$(base64 -w0 /tmp/guest-portal.p12)",
"password": "$PKCS12_PASSWORD"
}
EOF
echo "=== Certificate renewal complete ==="
cloudflare-credentials.ini: |
dns_cloudflare_api_token = ${CF_API_TOKEN}
1.2 CronJob Manifest
apiVersion: batch/v1
kind: CronJob
metadata:
name: ise-guest-cert-renewal
namespace: cert-automation
spec:
# Run at 2 AM on the 1st and 15th of each month
schedule: "0 2 1,15 * *"
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: certbot
image: certbot/dns-cloudflare:latest
command: ["/scripts/renew-and-push.sh"]
env:
- name: CF_API_TOKEN
valueFrom:
secretKeyRef:
name: cloudflare-dns
key: api-token
- name: ISE_USER
valueFrom:
secretKeyRef:
name: ise-api
key: username
- name: ISE_PASS
valueFrom:
secretKeyRef:
name: ise-api
key: password
- name: PKCS12_PASSWORD
valueFrom:
secretKeyRef:
name: ise-api
key: password
volumeMounts:
- name: scripts
mountPath: /scripts
- name: certs
mountPath: /etc/letsencrypt
volumes:
- name: scripts
configMap:
name: cert-automation-scripts
defaultMode: 0755
- name: certs
persistentVolumeClaim:
claimName: letsencrypt-certs
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: letsencrypt-certs
namespace: cert-automation
spec:
accessModes:
- ReadWriteOnce
storageClassName: nfs-client
resources:
requests:
storage: 100Mi
Phase 2: Deploy
# Apply manifests
kubectl apply -f cert-automation-configmap.yaml
kubectl apply -f cert-automation-cronjob.yaml
# Verify
kubectl get cronjob -n cert-automation
kubectl get pvc -n cert-automation
Phase 3: Manual Test
# Trigger job manually
kubectl create job --from=cronjob/ise-guest-cert-renewal test-run -n cert-automation
# Watch logs
kubectl logs -f job/test-run -n cert-automation
# Check job status
kubectl get jobs -n cert-automation
Phase 4: Verification
# Check ISE certificate
netapi ise api-call openapi GET '/api/v1/certs/system-certificate' | \
jq '.response[] | select(.usedBy | contains("Portal"))'
# Or via openssl
echo | openssl s_client -connect guest.domusdigitalis.dev:443 2>/dev/null | \
openssl x509 -noout -dates -issuer
Troubleshooting
DNS Challenge Fails
# Verify Cloudflare token has Zone:DNS:Edit permission
curl -X GET "https://api.cloudflare.com/client/v4/user/tokens/verify" \
-H "Authorization: Bearer $CF_API_TOKEN"
# Check DNS propagation
dig TXT _acme-challenge.guest.{domain-public}
Security Considerations
-
Cloudflare API token is scoped to DNS only (not full account access)
-
ISE credentials stored in k8s Secrets (consider Vault Agent injection)
-
PKCS12 password should be unique, not reused from ISE password
-
PVC stores Let’s Encrypt account key - backup if needed
References
-
Vault PKI Cert Issuance - Internal certificates