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}

ISE API Import Fails

# Test ERS connectivity
curl -k -u "$ISE_USER:$ISE_PASS" \
  "https://ise-01.inside.domusdigitalis.dev/ers/config/deploymentinfo/versioninfo"

# Check certificate format
openssl pkcs12 -info -in /tmp/guest-portal.p12 -passin pass:$PKCS12_PASSWORD

CronJob Not Running

# Check schedule
kubectl get cronjob -n cert-automation -o wide

# Check events
kubectl describe cronjob ise-guest-cert-renewal -n cert-automation

# Check for failed jobs
kubectl get jobs -n cert-automation --field-selector status.successful=0

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