docs/documentation/platform/pki/guides/applications/k8s-cert-manager-issuer.mdx
Issue TLS certificates to your Kubernetes workloads using cert-manager and the Infisical Issuer, an external issuer for cert-manager. Certificates are signed by Infisical through a PKI Application and Certificate Profile, stored in Kubernetes Secrets (including the CA chain), and renewed automatically before expiration.
<Info> This is the **Infisical Issuer** approach. If you would rather use the standard cert-manager ACME issuer with EAB credentials, see the [cert-manager (ACME) guide](/documentation/platform/pki/guides/applications/k8s-cert-manager-acme) instead.This guide assumes you have a PKI Application with API enrollment configured. </Info>
ca.crt automatically (with the ACME issuer this is not automatic and needs extra setup such as trust-manager).spiffe:// URIs for service meshes).Issuer (or ClusterIssuer) that authenticates to Infisical with a machine identity and points at your PKI Application and Profile.Certificate resources that define the certificates you need.Throughout this guide, replace <namespace> with the namespace where your workloads and certificates live. With a namespaced Issuer, its credentials Secret, the Issuer, and the Certificate all live in this namespace. To issue across namespaces from a single resource, use a ClusterIssuer instead, whose Secret and service account live in the controller's namespace rather than alongside each workload.
- A **PKI Application** with a **Certificate Profile** that uses **API enrollment**. Note the **Application name** (for example `web-services`) and the **Profile name** (for example `web-server`); you reference both by name when you create the `Issuer`.
- A **machine identity** added to the Application with the **operator** role, which lets it issue certificates.
Pick one authentication method for the identity:
- **Universal Auth**: use the identity's **Client ID** and **Client Secret**.
- **Kubernetes Auth**: the controller mints a short-lived token for a Kubernetes service account and presents it to Infisical. Configure it on the identity first, and note its **Identity ID**.
```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml
```
```bash
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
helm repo update
helm install infisical-pki-issuer infisical-helm-charts/infisical-pki-issuer \
--namespace infisical-pki-issuer --create-namespace
```
This installs the controller and registers the `Issuer` and `ClusterIssuer` custom resources in the `infisical-issuer.infisical.com` API group. Confirm the controller is running:
```bash
kubectl get pods -n infisical-pki-issuer
```
```yaml infisical-issuer-approver.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: infisical-issuer-approver
rules:
- apiGroups: ["cert-manager.io"]
resources: ["signers"]
verbs: ["approve"]
resourceNames:
- "issuers.infisical-issuer.infisical.com/*"
- "clusterissuers.infisical-issuer.infisical.com/*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: infisical-issuer-approver
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: infisical-issuer-approver
subjects:
- kind: ServiceAccount
name: cert-manager
namespace: cert-manager
```
```bash
kubectl apply -f infisical-issuer-approver.yaml
```
<Note>
If you skip this step, `Certificate` resources stay pending because their `CertificateRequest` is never approved, so the Infisical Issuer never signs it. Adjust the `ServiceAccount` name and namespace if your cert-manager runs under different ones.
</Note>
<Tabs>
<Tab title="Universal Auth">
```bash
kubectl create secret generic infisical-credentials \
--namespace <namespace> \
--from-literal=clientId=<machine_identity_client_id> \
--from-literal=clientSecret=<machine_identity_client_secret>
```
</Tab>
<Tab title="Kubernetes Auth">
Store the machine identity's **Identity ID** (Kubernetes Auth does not use a client secret):
```bash
kubectl create secret generic infisical-identity \
--namespace <namespace> \
--from-literal=identityId=<machine_identity_id>
```
The controller mints a short-lived token (via the Kubernetes `TokenRequest` API) for a service account you reference in the next step, then presents it to Infisical. Create that service account if it does not exist:
```bash
kubectl create serviceaccount infisical-issuer-auth --namespace <namespace>
```
The service account's name and namespace must be allowed by the identity's [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) configuration in Infisical (and the audience there must match), otherwise issuance fails authentication.
</Tab>
</Tabs>
<Tabs>
<Tab title="Universal Auth">
```yaml issuer-infisical.yaml
apiVersion: infisical-issuer.infisical.com/v1alpha1
kind: Issuer
metadata:
name: infisical-issuer
namespace: <namespace>
spec:
# Base URL of your Infisical instance
# (https://app.infisical.com, https://eu.infisical.com, or your self-hosted URL)
url: https://app.infisical.com
# Name of the PKI Application to issue through
application: <application_name>
# Name of the Certificate Profile to issue with
profile: <profile_name>
authentication:
method: universal
universal:
clientIdRef:
name: infisical-credentials
namespace: <namespace>
key: clientId
clientSecretRef:
name: infisical-credentials
namespace: <namespace>
key: clientSecret
```
</Tab>
<Tab title="Kubernetes Auth">
```yaml issuer-infisical.yaml
apiVersion: infisical-issuer.infisical.com/v1alpha1
kind: Issuer
metadata:
name: infisical-issuer
namespace: <namespace>
spec:
url: https://app.infisical.com
application: <application_name>
profile: <profile_name>
authentication:
method: kubernetes
kubernetes:
# Secret holding the machine identity ID
identityIdRef:
name: infisical-identity
namespace: <namespace>
key: identityId
# Service account whose token is presented to Infisical
# (the one created in the previous step)
serviceAccountRef:
name: infisical-issuer-auth
namespace: <namespace>
```
</Tab>
</Tabs>
```bash
kubectl apply -f issuer-infisical.yaml
```
Confirm the issuer is ready. The controller authenticates with Infisical and checks that the Application and Profile exist, then records the result on the issuer's `Ready` condition:
```bash
kubectl get issuers.infisical-issuer.infisical.com infisical-issuer \
-n <namespace> \
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}{"\n"}'
```
```bash
True
```
If this is not `True`, run `kubectl describe issuers.infisical-issuer.infisical.com infisical-issuer -n <namespace>` and read the `Ready` condition message for the reason (for example a missing secret, or an unknown Application or Profile).
<Note>
- An `Issuer` is namespace-scoped: certificates can only be issued from an `Issuer` in the same namespace as the `Certificate`. Its referenced Secrets (and service account) must live in that same namespace; cross-namespace references are rejected.
- To issue across namespaces with one resource, create a `ClusterIssuer` instead. The spec is identical except `kind: ClusterIssuer` and no `metadata.namespace`. Because a `ClusterIssuer` has no namespace of its own, its referenced Secrets and service account must live in the controller's namespace (the one the issuer is installed into, configurable with the controller's `--cluster-resource-namespace` flag), and each reference's `namespace` must match it.
- For a self-hosted Infisical that uses a private CA, add a `tls` block so the controller trusts it:
```yaml
spec:
tls:
caCertificate:
name: <secret_with_ca_cert>
namespace: <namespace>
key: ca.crt
```
</Note>
```yaml certificate.yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com
namespace: <namespace>
spec:
secretName: example-com-tls
commonName: example.com
dnsNames:
- example.com
# Optional. If omitted, the Certificate Profile's default TTL is used.
duration: 720h
# cert-manager renews before expiry
renewBefore: 240h
privateKey:
algorithm: RSA
size: 2048
issuerRef:
name: infisical-issuer
kind: Issuer
group: infisical-issuer.infisical.com
```
```bash
kubectl apply -f certificate.yaml
```
Check that it was issued:
```bash
kubectl get certificate -n <namespace>
```
```bash
NAME READY SECRET AGE
example-com True example-com-tls 15s
```
<Note>
Workload identity certificates work too. For a SPIFFE identity (for example with istio-csr), set a `uris` SAN and make sure the Certificate Profile policy allows URI SANs:
```yaml
spec:
uris:
- spiffe://cluster.local/ns/default/sa/my-workload
```
</Note>
```bash
kubectl get secret example-com-tls -n <namespace>
```
```bash
NAME TYPE DATA AGE
example-com-tls kubernetes.io/tls 3 1m
```
The Secret contains three keys: `tls.crt` (the issued certificate plus any intermediates), `tls.key` (the private key), and `ca.crt` (the root CA). You can decode the certificate with `openssl`:
```bash
kubectl get secret example-com-tls -n <namespace> \
-o jsonpath='{.data.tls\.crt}' | base64 --decode | openssl x509 -text -noout
```
The Secret is now ready to be mounted by your workloads (Ingresses, Deployments, service meshes, and so on).
- **Universal Auth** is the simplest: store a Client ID and Client Secret in a Secret. Good for any cluster.
- **Kubernetes Auth** avoids storing a long-lived secret: the controller mints a short-lived token for a Kubernetes service account, and Infisical validates it against your cluster. Prefer this when you want to bind issuance to a service account rather than a static secret. It requires configuring [Kubernetes Auth](/documentation/platform/identities/kubernetes-auth) on the identity first.