> ## Documentation Index
> Fetch the complete documentation index at: https://docs.certgov.app/llms.txt
> Use this file to discover all available pages before exploring further.

# cert-manager Integration

> Route Kubernetes certificate requests through CertForge policy using the certforge-issuer external issuer.

The `certforge-issuer` is a cert-manager [external issuer](https://cert-manager.io/docs/contributing/external-issuers/) that intercepts `CertificateRequest` objects and submits them to CertForge for policy evaluation, approval, and issuance. Your existing `Certificate` manifests work unchanged — you just point `issuerRef` at CertForge instead of a built-in issuer.

```
Pod → cert-manager → certforge-issuer → CertForge API → CA
                                      ← signed cert   ←
```

## Prerequisites

* Kubernetes 1.24+
* cert-manager v1.14+
* A CertForge account with at least one Domain Trust Profile covering your cluster's domains

## Before you install

CertForge must be configured for the domains your workloads will request certificates for, otherwise requests will be rejected immediately with `InvalidRequest`.

**1. Create a Domain Trust Profile** covering the domain patterns your workloads use. For example, if pods will request certs for `*.internal.example.com`, your DTP must include that pattern. Go to **Domain Trust Profiles → New** in the CertForge dashboard.

**2. Generate an API token** — go to **Settings → API Keys** and create a token with the **enroll** and **read** scopes. No other scopes are required. You will supply this during installation.

## Installation

### Helm (recommended)

**US East (default):**

```bash theme={null}
helm install certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --create-namespace \
  --set certforge.url=https://app.certgov.app \
  --set certforge.token=<your-api-token>
```

**EU West (GDPR-aligned):**

```bash theme={null}
helm install certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --create-namespace \
  --set certforge.url=https://eu.certgov.app \
  --set certforge.token=<your-eu-api-token>
```

**Self-hosted:** replace the URL with your CertForge instance URL:

```bash theme={null}
helm install certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --create-namespace \
  --set certforge.url=https://certforge.internal.example.com \
  --set certforge.token=<your-api-token>
```

### Manual

```bash theme={null}
kubectl apply -f https://raw.githubusercontent.com/CertForge-LLC/certforge-issuer/main/config/crd/certforge-issuer.yaml
kubectl apply -f https://raw.githubusercontent.com/CertForge-LLC/certforge-issuer/main/config/rbac/rbac.yaml

kubectl create secret generic certforge-credentials \
  --namespace certforge-system \
  --from-literal=token=<your-api-token>

kubectl apply -f https://raw.githubusercontent.com/CertForge-LLC/certforge-issuer/main/config/manager/deployment.yaml
```

## Configure the issuer

### Namespaced issuer

Use `CertForgeIssuer` to scope issuance to a single namespace:

```yaml theme={null}
apiVersion: certforge.io/v1alpha1
kind: CertForgeIssuer
metadata:
  name: certforge
  namespace: default
spec:
  url: https://app.certgov.app
  authSecretRef:
    name: certforge-credentials
```

### Cluster-wide issuer

Use `CertForgeClusterIssuer` to issue certificates across all namespaces. The credentials secret must live in the `certforge-system` namespace:

```yaml theme={null}
apiVersion: certforge.io/v1alpha1
kind: CertForgeClusterIssuer
metadata:
  name: certforge
spec:
  url: https://app.certgov.app
  authSecretRef:
    name: certforge-credentials
```

## Data residency

Your API token is scoped to a single region at account creation time. Set `url` to match your org's region — the URL and token together route every certificate request to the correct region with no cross-border data flows.

| Region                 | URL                       |
| ---------------------- | ------------------------- |
| US East (default)      | `https://app.certgov.app` |
| EU West (GDPR-aligned) | `https://eu.certgov.app`  |
| Self-hosted            | Your instance URL         |

To serve multiple regions from one cluster, deploy a separate `CertForgeClusterIssuer` per region with its own credentials secret:

```yaml theme={null}
apiVersion: certforge.io/v1alpha1
kind: CertForgeClusterIssuer
metadata:
  name: certforge-eu
spec:
  url: https://eu.certgov.app
  authSecretRef:
    name: certforge-eu-credentials
  secretNamespace: certforge-system
```

```bash theme={null}
kubectl create secret generic certforge-eu-credentials \
  --namespace certforge-system \
  --from-literal=token=<your-eu-api-token>
```

See [Data Residency](/architecture#data-residency) for the full regional architecture.

## Request a certificate

Point any cert-manager `Certificate` at CertForge by changing `issuerRef`:

```yaml theme={null}
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: my-service-tls
  namespace: default
spec:
  secretName: my-service-tls
  dnsNames:
    - my-service.internal.example.com
  issuerRef:
    name: certforge
    kind: CertForgeIssuer   # or CertForgeClusterIssuer
    group: certforge.io
```

cert-manager creates a `CertificateRequest`; the issuer controller submits the CSR to CertForge; once issued the certificate is written back as a Kubernetes Secret.

### Wildcard certificates

Wildcard certificates work the same way — ensure your DTP permits wildcards for the domain:

```yaml theme={null}
spec:
  dnsNames:
    - "*.internal.example.com"
  issuerRef:
    name: certforge
    kind: CertForgeClusterIssuer
    group: certforge.io
```

### Multiple SANs

```yaml theme={null}
spec:
  dnsNames:
    - api.internal.example.com
    - api-v2.internal.example.com
  ipAddresses:
    - 10.0.1.50
  issuerRef:
    name: certforge
    kind: CertForgeIssuer
    group: certforge.io
```

## How approval works

Certificate issuance involves two separate approval steps:

**1. cert-manager approval** — cert-manager's built-in approver only auto-approves requests for its own built-in issuers (ACME, CA, SelfSigned). For `certforge-issuer`, the `CertificateRequest` will sit with an empty `APPROVED` column until it is approved. The certforge-issuer controller will not submit the CSR to CertForge until this condition is set.

For production, install [cert-manager-approver-policy](https://cert-manager.io/docs/policy/approval/approver-policy/) and deploy a `CertificateRequestPolicy` that auto-approves requests for `certforge.io` issuers. CertForge already enforces domain policy via Domain Trust Profiles, so the Kubernetes-side rule just needs to unblock the approval gate:

```bash theme={null}
# 1. Install cert-manager-approver-policy
helm repo add jetstack https://charts.jetstack.io && helm repo update
helm upgrade cert-manager-approver-policy \
  jetstack/cert-manager-approver-policy \
  --install --namespace cert-manager --wait

# 2. Disable the built-in approver (required when using approver-policy)
helm upgrade cert-manager jetstack/cert-manager \
  --namespace cert-manager --reuse-values \
  --set disableAutoApproval=true
```

```yaml theme={null}
# 3. Apply the policy + required RBAC
apiVersion: policy.cert-manager.io/v1alpha1
kind: CertificateRequestPolicy
metadata:
  name: certforge-auto-approve
spec:
  allowed:
    commonName:
      value: "*"
    dnsNames:
      values:
        - "*"
    ipAddresses:
      values:
        - "*"
    uris:
      values:
        - "*"
    usages:
      - signing
      - digital signature
      - key encipherment
      - server auth
      - client auth
  selector:
    issuerRef:
      group: certforge.io
---
# approver-policy requires RBAC to bind the policy to the cert-manager
# controller service account. Without this, every CertificateRequest will
# be logged as "not applicable for any policy" and never auto-approved.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: cert-manager-certforge-auto-approve
rules:
  - apiGroups: ["policy.cert-manager.io"]
    resources: ["certificaterequestpolicies"]
    verbs: ["use"]
    resourceNames: ["certforge-auto-approve"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: cert-manager-certforge-auto-approve
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cert-manager-certforge-auto-approve
subjects:
  - kind: ServiceAccount
    name: cert-manager
    namespace: cert-manager
```

The policy matches both `CertForgeIssuer` and `CertForgeClusterIssuer` (anything in the `certforge.io` group). For namespace-scoped or domain-scoped variants, see the [sample file](https://github.com/CertForge-LLC/certforge-issuer/blob/main/config/samples/approver-policy.yaml) in the certforge-issuer repository.

For local testing, approve manually with `cmctl`:

```bash theme={null}
# Install cmctl (Linux amd64)
curl -L -o /tmp/cmctl https://github.com/cert-manager/cmctl/releases/latest/download/cmctl_linux_amd64
chmod +x /tmp/cmctl && sudo mv /tmp/cmctl /usr/local/bin/cmctl

# Approve a pending request
cmctl approve <certificaterequest-name> -n <namespace>
```

**2. CertForge approval** — once the CertificateRequest is approved in Kubernetes, the behavior depends on your Domain Trust Profile:

* **Auto-approve** — the certificate is issued immediately, typically within a few seconds.
* **Manual approval required** — the request enters the CertForge approval queue. The controller polls until the request is approved or denied. The `Certificate` stays pending until then.

Approvers can act on requests from the CertForge dashboard under **Approvals**.

## Troubleshooting

If a `Certificate` stays pending, inspect the underlying `CertificateRequest`:

```bash theme={null}
kubectl describe certificaterequest <name> -n <namespace>
```

| Condition             | Reason            | Cause                                                                                           |
| --------------------- | ----------------- | ----------------------------------------------------------------------------------------------- |
| `InvalidRequest=True` | `PolicyViolation` | Domain not covered by any DTP, or wildcard not permitted — add the domain to a DTP in CertForge |
| `Denied=True`         | `Denied`          | Request was manually denied in the CertForge approval queue                                     |
| `Ready=False`         | `Pending`         | Waiting for manual approval, or transient connectivity issue                                    |
| `Ready=False`         | `Failed`          | API token invalid or expired — regenerate under Settings → API Keys                             |

Check the issuer controller logs for connectivity errors:

```bash theme={null}
kubectl logs -n certforge-system deployment/certforge-issuer
```

**Certificate stuck pending for more than a few minutes with auto-approve DTP:**
Check that the controller can reach CertForge — network policies or egress rules may be blocking the connection. The controller needs outbound HTTPS to the CertForge URL.

**`x509: certificate signed by unknown authority`:**
Your self-hosted CertForge instance uses a CA the controller doesn't trust. Add the CA certificate to your cluster's trusted roots (e.g. via a custom CA bundle in the node's trust store, or by configuring the certforge-issuer pod with a volume-mounted CA cert and the `SSL_CERT_FILE` environment variable pointing to it).

**`API token invalid` in controller logs:**
The token in the `certforge-credentials` secret has expired or been revoked. Generate a new one under **Settings → API Keys** and update the secret:

```bash theme={null}
kubectl create secret generic certforge-credentials \
  --namespace certforge-system \
  --from-literal=token=<new-token> \
  --dry-run=client -o yaml | kubectl apply -f -
```

For full controller configuration options and CRD field reference, see the [certforge-issuer guide](/guides/certforge-issuer).
