STACKIT Webhook Integration for Cert Manager

Facilitate a webhook integration for leveraging the STACKIT DNS alongside its API to act as a DNS01 ACME Issuer with cert-manager.
Installation
helm repo add stackit-cert-manager-webhook https://stackitcloud.github.io/stackit-cert-manager-webhook
helm install stackit-cert-manager-webhook --namespace cert-manager stackit-cert-manager-webhook/stackit-cert-manager-webhook
Authentication & Usage
The STACKIT webhook requires authentication against the STACKIT DNS API. Depending on your cluster architecture and security policies, you can authenticate using one of the three methods below.
The webhook will explicitly fail if multiple mutually exclusive authentication methods are configured for a single Issuer.
Option A: Dynamic Service Account Key (Multi-Tenant)
This method is recommended for multi-tenant clusters where different Issuer or ClusterIssuer resources manage zones across different STACKIT projects. The webhook fetches the Service Account JSON directly from a Kubernetes Secret per challenge.
-
Create the Secret containing the SA JSON:
kubectl create secret generic stackit-tenant-a-auth \
-n default \
--from-file=sa.json=/path/to/tenant-a-sa.json
-
Configure the Issuer:
Ensure the serviceAccountSecretNamespace matches the namespace of your Secret. If you want the webhook to read secrets outside of its own installation namespace, you must set stackitSaAuthentication.secretAccessScope=issuer when installing the Helm chart.
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: letsencrypt-prod
namespace: default
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: example@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
webhook:
solverName: stackit
groupName: acme.stackit.de
config:
projectId: <STACKIT ID PROJECT>
serviceAccountSecretRef: stackit-tenant-a-auth
serviceAccountSecretKey: sa.json
serviceAccountSecretNamespace: default
Option B: Static Service Account Key (Single Tenant / Global Fallback)
This method mounts a single Service Account key JSON file into the webhook Pod. It is ideal for single-tenant clusters where the webhook manages domains for a single STACKIT project or organization.
-
Deploy the Webhook with the Key Mounted:
Create a secret in the cert-manager namespace and install the Helm chart with mounting enabled:
kubectl create secret generic stackit-sa-authentication \
-n cert-manager \
--from-file=sa.json=/path/to/global-sa.json
helm upgrade --install stackit-cert-manager-webhook stackit-cert-manager-webhook/stackit-cert-manager-webhook \
--namespace cert-manager \
--set stackitSaAuthentication.enabled=true
-
Configure the Issuer:
Reference the mounted file path.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: example@example.com
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- dns01:
webhook:
solverName: stackit
groupName: acme.stackit.de
config:
projectId: <STACKIT ID PROJECT>
serviceAccountKeyPath: /var/run/secrets/stackit/sa.json
Option C: Workload Identity Federation (WIF)
If your cluster supports Workload Identity Federation (e.g., SKE clusters), you can avoid managing long-lived keys entirely by projecting a short-lived token into the webhook container.
-
Annotate the Webhook ServiceAccount:
Update your Helm deployment to instruct the identity webhook to inject the federated token.
# values.yaml
serviceAccount:
annotations:
workload-identity.stackit.cloud/service-account-email: "your-service-account@sa.stackit.cloud"
-
Configure the Issuer:
Explicitly instruct the webhook to use the WIF flow.
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
# ...
solvers:
- dns01:
webhook:
solverName: stackit
groupName: acme.stackit.de
config:
projectId: <STACKIT ID PROJECT>
useWorkloadIdentityFederation: true
Config Options
The following table delineates the configuration options available under the config block of the STACKIT Cert Manager Webhook solver:
| Key |
Type |
Default |
Description |
projectId |
string |
"" |
Required. The unique identifier for the STACKIT project. |
apiBasePath |
string |
"https://dns.api.stackit.cloud" |
The base path for the STACKIT DNS API. |
serviceAccountSecretRef |
string |
"" |
Name of the Kubernetes Secret containing the SA JSON. |
serviceAccountSecretKey |
string |
"" |
The key within the Secret mapped to the JSON content. |
serviceAccountSecretNamespace |
string |
<webhook-namespace> |
The namespace where the Secret is located. |
serviceAccountKeyPath |
string |
"" |
The absolute file path to a statically mounted SA JSON key inside the webhook container. |
useWorkloadIdentityFederation |
bool |
false |
Explicitly enables STACKIT Workload Identity Federation authentication. |
serviceAccountBaseUrl |
string |
"" |
Custom URL for trading SA keys for access tokens. |
acmeTxtRecordTTL |
int32 |
60 |
The TTL for the ACME TXT challenge record. |
Test Procedures
Unit Testing:
make test
Unit Testing with Coverage Analysis:
make coverage
Linting:
make lint
Runs the official cert-manager Go solver test suite in memory against the STACKIT API:
TEST_ZONE_NAME="example.com" make test-e2e-conformance
Follow the comprehensive guide available here.
Kubernetes Integration (E2E) Testing:
Spins up a local Kind cluster, installs cert-manager, builds and deploys the webhook, and executes Kuttl integration tests covering both single-tenant (static fallback) and multi-tenant (dynamic SA fetching) flows against Let's Encrypt Staging:
make test-e2e-local \
PROJECT_ID="<your-project-id>" \
ZONE_NAME="<your-test-zone>" \
AUTH_KEY_PATH="<path-to-sa-key.json>"
Follow the comprehensive guide available here.
Release Process Overview
Our release pipeline leverages goreleaser for the generation and publishing of release assets.
This sophisticated approach ensures the streamlined delivery of:
- Pre-compiled binaries tailored for various platforms.
- Docker images optimized for production readiness.
However, one should be cognizant of the fact that goreleaser doesn't inherently support Helm chart distributions
as part of its conventional workflow. Historically, the incorporation of Helm charts into our releases demanded manual
intervention. Post the foundational release generation via goreleaser, the Helm chart was affixed as an asset through
manual processes.
For those interested in the Helm chart creation mechanics, the process was facilitated via the command:
helm package deploy/stackit
To release a new version of the Helm chart, one must meticulously update the appVersion and (chart)version delineation in the
Chart.yaml. Post this modification, initiate a new release to encompass these changes.