Skip to content
Install the CLI

How-to guide Package task guides

Configure cert-manager issuers

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 an empty aliases list:

clusters/test-doc-how-tos.nix
instances.cert-manager.letsencrypt-staging = {
package = packages.cluster-issuer;
# An empty list replaces the default alias `clusterIssuer`, so
# only the production issuer answers it.
aliases = [ ];
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, so the staging issuer answers no alias. A package that needs it names it with deps.clusterIssuer = ref.cert-manager.letsencrypt-staging.

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

how-to/homelab/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. Because cluster-issuer sets meta.scope = "cluster" and dependency resolution prefers a managed instance over an import, 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. cert-manager creates the HTTP-01 solver pods at runtime in each Certificate’s namespace, and they listen on TCP 8089. When the cluster enforces network policy, kix.expose admits the ingress controller to those pods for each Ingress it builds with TLS from this issuer. A Certificate you create another way needs that traffic admitted too, for example with a platform intent as in Declare platform intents for non-Kix-managed pods.

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.