Skip to content
kix /docs
Install the CLI

How-to guide Package task guides

Configure cert-manager issuers

Install cert-manager and add ClusterIssuers for ACME DNS-01 certificates or local self-signed certificates.

Kix separates the cert-manager controller from issuer configuration. Install cert-manager, then add one cluster-issuer instance for each ClusterIssuer. The ClusterIssuer takes the instance’s name, and config.spec is its spec in cert-manager’s API.

Put every cluster-issuer instance in cert-manager’s namespace. cert-manager reads a ClusterIssuer’s Secrets only from its own namespace, so the issuer’s credentials must be there, and a Kix Secret serves only the namespace it is declared in. Instance names are unique within a namespace, so two issuers cannot render the same ClusterIssuer name. An instance in another namespace fails the build and names the namespace to move it to.

For public certificates, give the ACME server, the account email, and a solver. This example uses Cloudflare DNS-01:

clusters/test-doc-how-tos.nix
instances.cert-manager.cert-manager = {
package = packages.cert-manager;
};
# A Secret created outside Kix that holds the Cloudflare API token
# under the key `api-token`.
instances.cert-manager.cloudflare-api-token = {
package = packages.secret-ref;
config.keys = [ "api-token" ];
};
instances.cert-manager.letsencrypt = {
package = packages.cluster-issuer;
deps.issuerCredentials = ref.cert-manager.cloudflare-api-token;
config.spec.acme = {
server = "https://acme-v02.api.letsencrypt.org/directory";
email = "[email protected]";
solvers = [
{
dns01.cloudflare.apiTokenSecretRef.key = "api-token";
selector.dnsZones = [ "example.com" ];
}
];
};
};

View source on GitHub ↗

The cloudflare-api-token instance declares a Secret created outside Kix that holds the token under the key api-token. Use a Kix-managed Secret, a SOPS-backed Secret, or an externally managed Secret, in the cert-manager namespace.

deps.issuerCredentials wires that Secret to the issuer. A Kix Secret dependency is named for what it holds; issuerCredentials holds every key this issuer’s Secret key selectors name. Leave name out of each selector, such as apiTokenSecretRef, and set only key. Kix fills in the Secret’s name and fails the build when the Secret does not declare that key, so the ClusterIssuer cannot reach the cluster pointing at a Secret or key that is not there. To name a Secret that Kix should not check, write name = kix.untrackedRef "<secret>".

An issuer that needs several credentials, such as an ACME external account binding and a DNS token, keeps them as several keys of its one issuerCredentials Secret; each selector picks its own key.

Kix sets acme.privateKeySecretRef.name to <instance>-acme-account-key unless the spec sets it. cert-manager creates that Secret when it registers the ACME account. Restrict selector.dnsZones to the zones the token may update.

The spec is checked against the ClusterIssuer CRD that the cert-manager package vendors. A field the CRD does not define, a missing required field such as acme.server, a value of the wrong type (such as solvers written without list brackets), a spec with no issuer type or two, and a solver that sets both http01 and dns01 or two DNS providers each fail the build with the spec path and the accepted values. A field set to null is left out.

external-dns reads its Cloudflare token from a Secret in its own namespace. Give that Secret the same key, so one source feeds both.

Every cluster-issuer instance answers the clusterIssuer dependency that TLS-capable packages read, so a second issuer in the same namespace makes that dependency ambiguous. Give the staging issuer a non-empty aliases list:

clusters/test-doc-how-tos.nix
instances.cert-manager.letsencrypt-staging = {
package = packages.cluster-issuer;
# A non-empty alias replaces the default `clusterIssuer`, so only
# the production issuer answers it.
aliases = [ "stagingClusterIssuer" ];
deps.issuerCredentials = ref.cert-manager.cloudflare-api-token;
config.spec.acme = {
server = "https://acme-staging-v02.api.letsencrypt.org/directory";
email = "[email protected]";
solvers = [
{
dns01.cloudflare.apiTokenSecretRef.key = "api-token";
selector.dnsZones = [ "example.com" ];
}
];
};
};

View source on GitHub ↗

Instance aliases replace the package’s default alias. The list must not be empty, because an empty list falls back to the default, so it names an alias nothing reads. Kix has no way yet to say that an instance answers no alias.

For a local cluster, use a self-signed issuer:

ludo/variants/cluster-kind-tls.nix
instances.cert-manager.selfsigned = {
package = packages.cluster-issuer;
config.spec.selfSigned = { };
};

View source on GitHub ↗

A self-signed issuer tests certificate creation and TLS wiring without an ACME account or DNS provider. Clients will not trust its certificates unless you install the generated trust material, so do not use this configuration for a public service.

When cert-manager and its ClusterIssuer come from Helm or a platform add-on, Kix does not own them and the cluster-issuer package cannot be used: it needs the cert-manager package’s CRD handle, schema, and namespace. Declare the existing issuer with kix.mkImport and the clusterIssuer alias instead:

clusters/test-doc-how-tos.nix
instances.external-tls.cluster-issuer = {
package = kix.mkImport {
out.withCert =
args:
args
// {
annotations = (args.annotations or { }) // {
"cert-manager.io/cluster-issuer" = "letsencrypt";
};
};
};
aliases = [ "clusterIssuer" ];
};

View source on GitHub ↗

Kix then checks nothing about the issuer beyond the name the import gives it. A cluster-issuer instance that answers clusterIssuer anywhere in the cluster takes precedence over the import, so use one or the other. The example cluster has both, which is why its consumer of the import sets deps.clusterIssuer explicitly.

The spec accepts an http01 solver, and the build warns when the cluster enforces network policy. cert-manager creates the HTTP-01 solver pods at runtime in each Certificate’s namespace, and Kix’s derived policy does not admit the ingress controller to them, so the challenge does not complete unless something else admits that traffic. Use a DNS-01 solver, or admit the solver pods on TCP 8089 with a platform intent as in Declare platform intents for non-Kix-managed pods. The warning does not read platform intents, so it remains after you add one.

Kix fills and checks only Secret key selectors and the ACME account key. ca.secretName, venafi.*.credentialsRef.name, vault.auth.kubernetes.serviceAccountRef.name, the HTTP-01 solver’s Ingress, IngressClass, and Gateway references, and anything inside a webhook solver’s config pass through as written. Kix does not check that those objects exist.

Evaluate the cluster before applying it:

❱ kix check doc-how-tos

Deploy cert-manager, its CRDs, and the ClusterIssuers:

❱ kix deploy doc-how-tos

A deploy waits for each ClusterIssuer, and each Certificate built from certManager.out.crds.Certificate, to report Ready. An ACME ClusterIssuer reports Ready once cert-manager has registered its account. Kix does not wait for the Certificates that cert-manager creates from Ingress annotations. ACME registration and DNS-01 issuance of the Certificates Kix waits for can take longer than the default readiness timeout of 180 seconds; raise it with --timeout on clusters that issue through ACME:

❱ kix deploy doc-how-tos --timeout 15m

After deployment, inspect issuer readiness:

❱ kubectl get clusterissuers
❱ kubectl describe clusterissuer letsencrypt

For ACME failures, check the credential key, the zone restriction, DNS delegation, and the cert-manager controller logs.

See Add TLS through a cluster issuer to use the issuer from an application package.