A cert-manager DNS01 webhook that solves ACME challenges using Thalassa Cloud DNS. Use it to obtain TLS certificates from Let's Encrypt (or other ACME issuers) for domains whose DNS is hosted on Thalassa Cloud.
- cert-manager installed in your cluster (v1.20+)
- A Thalassa Cloud account with at least one DNS zone
- Your domain's nameservers delegated to Thalassa Cloud (or the zone created there)
- Personal access token from the Thalassa Cloud dashboard
- Your organisation identity (and optionally a project identity if the zone is project-scoped)
The webhook implements the cert-manager DNS01 challenge flow:
- Present: Creates a TXT record in your Thalassa Cloud DNS zone with the ACME challenge value.
- CleanUp: Removes that TXT record after the challenge is validated.
Cert-manager calls this webhook when you request a certificate that uses a DNS01 solver configured with the thalassa provider. DNS changes may take ~25–40s to propagate.
Deploy the webhook into your cluster using your preferred method (Helm chart, raw manifests, etc.). The webhook must run with:
- GROUP_NAME: Environment variable set to the same group name used in your
ClusterIssuer/Issuer(e.g.acme.yourdomain.com).
Ensure the webhook is reachable by cert-manager and that the necessary RBAC and TLS serving configuration are in place (exact steps depend on your deployment method).
Create a Kubernetes Secret in the same namespace as the Certificate (or the namespace referenced by your Issuer) that holds your Thalassa Cloud personal access token:
apiVersion: v1
kind: Secret
metadata:
name: thalassa-credentials
type: Opaque
data:
accessKey: <base64-encoded personal access token>To base64-encode your token:
echo -n "your-thalassa-personal-access-token" | base64In your ClusterIssuer or Issuer, add a DNS01 solver that uses this webhook and the solver config:
spec:
acme:
solvers:
- dns01:
webhook:
groupName: acme.yourdomain.com
solverName: thalassa
config:
apiSecretRef:
name: thalassa-credentials
key: accessKey
organisation: my-org
# project: my-project # optional, when the zone is project-scoped
# baseURL: https://api.thalassa.cloud # optional, this is the default- groupName: Must match the
GROUP_NAMEenv var of the webhook deployment. - solverName: Must be
thalassa(the name implemented by this webhook). - config.apiSecretRef: Reference to the Secret and key that contain the personal access token.
- config.organisation: Thalassa Cloud organisation identity for API requests.
- config.project: Optional project identity when DNS zones are project-scoped.
- config.baseURL: Optional API base URL (defaults to
https://api.thalassa.cloud).
When using the Helm chart, a projected service account token is mounted at /var/run/thalassa-oidc/token with audience https://api.thalassa.cloud/. Before the webhook can call the DNS API, bootstrap workload identity so the webhook Kubernetes service account can authenticate:
# Thalassa-managed cluster (OIDC provider from label kubernetes_cluster_id)
tcloud iam workload-identity-federation bootstrap kubernetes --cluster my-cluster-slug \
--namespace <cert-manager-namespace> --service-account <cert-manager-webhook-thalassa-sa> \
--role dns:FullAccess
# Self-managed / custom issuer
tcloud iam workload-identity-federation bootstrap kubernetes --issuer https://k8s.example.com \
--namespace <cert-manager-namespace> --service-account <cert-manager-webhook-thalassa-sa> \
--role dns:FullAccessSet workloadIdentity.cluster or workloadIdentity.issuer in Helm values to print a tailored bootstrap command after helm install. Then configure your Issuer:
config:
organisation: my-org
serviceAccountID: sa-xxxxxxxx # from bootstrap outputNo long-lived API token secret is required. Disable the projected volume with projectedToken.enabled: false if you authenticate another way.
| Field | Description |
|---|---|
apiSecretRef.name |
Name of the Secret containing the personal access token. |
apiSecretRef.key |
Key within the Secret (e.g. accessKey). |
organisation |
Thalassa Cloud organisation identity. |
project |
Optional project identity for project-scoped DNS zones. |
baseURL |
Optional API base URL (default: https://api.thalassa.cloud). |
serviceAccountID |
Optional Thalassa service account for OIDC token exchange (uses the pod service account JWT by default). |
subjectTokenFile |
Optional path to a subject JWT file (default with the Helm chart: /var/run/thalassa-oidc/token). |
subjectTokenSecretRef |
Optional secret reference for a custom subject JWT instead of the pod service account token. |
clientID |
Optional OIDC client ID for client credentials flow. |
clientSecretRef |
Secret reference for the OIDC client secret (used with clientID). |
oidcTokenURL |
Optional OIDC token URL override. |
insecure |
Disable TLS certificate verification (development only). |
Configure one authentication method: apiSecretRef (personal token), serviceAccountID (OIDC token exchange), or clientID + clientSecretRef (client credentials).
Example config.json (as used in tests or custom deployments):
{
"apiSecretRef": {
"Name": "thalassa-credentials",
"Key": "accessKey"
},
"organisation": "my-org"
}apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-tls
namespace: default
spec:
secretName: example-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- "*.example.com"Your ClusterIssuer (or Issuer) must use the thalassa webhook solver as shown in the "Configure the DNS01 solver" section. The domain (e.g. example.com) must be a DNS zone in your Thalassa Cloud organisation (or project, if configured).
make buildBuilds a Docker image cert-manager-webhook-thalassa:latest.
The project uses the cert-manager DNS01 conformance suite.
- Copy and edit the example config and credentials under
testdata/thalassa/:config.json.example→config.jsonthalassa-credentials.yaml.example→thalassa-credentials.yaml
- Fill in your personal access token (base64 in the secret), organisation identity, and ensure the secret and config match the solver config (e.g. secret name and key).
- Set
TEST_ZONE_NAMEto a domain that exists as a DNS zone in your Thalassa Cloud account (trailing dot optional).
Then run:
TEST_ZONE_NAME=example.com. make testReplace example.com. with your actual zone name.
go vet ./...If you use golangci-lint (see .golangci.yml):
golangci-lint run