> ## 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.

# certforge-issuer Reference

> Configuration reference, CRD schema, Helm values, workload identity setup, and operational guide for the certforge-issuer controller.

`certforge-issuer` is the cert-manager [external issuer](https://cert-manager.io/docs/contributing/external-issuers/) controller that bridges Kubernetes `CertificateRequest` objects to the CertForge API. It runs as a Deployment in your cluster and watches for `CertificateRequest` resources directed at `CertForgeIssuer` or `CertForgeClusterIssuer` objects.

For installation and quick-start usage, see the [cert-manager Integration guide](/guides/cert-manager).

<img src="https://mintcdn.com/certforgellc/QmKL28tJspF_yVu_/images/k8s-architecture.png?fit=max&auto=format&n=QmKL28tJspF_yVu_&q=85&s=66c7d74949e84d7973fa94c7c7eb0aa0" alt="cert-manager + certforge-issuer architecture — Helm install, CRDs, namespace scoping, and certificate flow" width="1060" height="1241" data-path="images/k8s-architecture.png" />

## Version compatibility

| certforge-issuer | cert-manager | Kubernetes | CertForge API |
| ---------------- | ------------ | ---------- | ------------- |
| v0.2.x           | v1.14+       | 1.27+      | v1            |
| v0.1.x           | v1.14+       | 1.24+      | v1            |

Always use the latest patch release. Run `helm search repo certforge-issuer --versions` to list available releases.

***

## Helm chart values

Install or upgrade with `helm upgrade --install`:

```bash theme={null}
helm upgrade --install certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --create-namespace \
  --values values.yaml
```

### Full values reference

```yaml theme={null}
certforge:
  # URL of your CertForge instance (required)
  url: https://app.certgov.app

  # API token — stored as a Kubernetes Secret.
  # Required when using Secret-based auth (the default).
  # Not needed when workloadIdentity.enabled=true.
  token: ""

  # Optional: default issuance profile ID applied to all requests from this issuer
  issuanceProfileID: ""

  # Optional: override the namespace where the credentials Secret is looked up
  # (CertForgeClusterIssuer only — defaults to certforge-system)
  secretNamespace: ""

tokenSecret:
  # Set true to create the Secret from certforge.token above
  create: true
  name: certforge-credentials

replicaCount: 1

image:
  repository: ghcr.io/certforge-llc/certforge-issuer
  tag: ""        # defaults to chart appVersion
  pullPolicy: IfNotPresent

resources:
  requests:
    cpu: 10m
    memory: 32Mi
  limits:
    cpu: 100m
    memory: 64Mi

# Leader election — recommended when running multiple replicas
leaderElect: true

# Workload Identity — authenticate using projected ServiceAccount tokens
# instead of a long-lived API key stored in a Secret. See the
# "Workload Identity" section below for full setup instructions.
workloadIdentity:
  enabled: false
  audience: "https://app.certgov.app"  # must match the CertForge WI Provider
  expirationSeconds: 3600   # kubelet rotates token before this deadline
  tokenFile: "/var/run/secrets/certforge/token"

# NetworkPolicy — restrict pod traffic to only what the controller needs.
# Requires a CNI with NetworkPolicy support (Calico, Cilium, etc.).
networkPolicy:
  enabled: false
  kubeApiServerPort: 6443   # set to 443 for managed clusters (EKS, GKE, AKS)

# ServiceMonitor — Prometheus Operator integration.
# Requires prometheus-operator or kube-prometheus-stack.
serviceMonitor:
  enabled: false
  interval: 30s
  scrapeTimeout: 10s
  # Labels required by your Prometheus Operator's selector, e.g.:
  # additionalLabels: {release: prometheus}
  additionalLabels: {}
```

***

## CRD reference

### CertForgeIssuer

Namespaced resource. Issues certificates only for `CertificateRequest` objects in the same namespace.

Exactly one of `authSecretRef` or `workloadIdentity` must be set.

```yaml theme={null}
apiVersion: certforge.io/v1alpha1
kind: CertForgeIssuer
metadata:
  name: certforge
  namespace: default
spec:
  # URL of the CertForge instance (required)
  url: https://app.certgov.app

  # Option A: Secret-based auth (long-lived token)
  # Secret must be in the same namespace as this issuer.
  authSecretRef:
    name: certforge-credentials

  # Option B: Workload Identity (short-lived projected ServiceAccount token)
  # workloadIdentity:
  #   audience: https://app.certgov.app
  #   tokenFile: /var/run/secrets/certforge/token   # optional, this is the default

  # Optional: default issuance profile ID for requests handled by this issuer
  issuanceProfileID: ""
```

**Status conditions:**

| Condition | Status  | Meaning                                                |
| --------- | ------- | ------------------------------------------------------ |
| `Ready`   | `True`  | Issuer connected to CertForge and ready to sign        |
| `Ready`   | `False` | Cannot reach CertForge — check URL, token, and network |

```bash theme={null}
kubectl get certforgeissuer certforge -n default -o yaml
```

### CertForgeClusterIssuer

Cluster-scoped resource. Issues certificates for `CertificateRequest` objects in any namespace. When using Secret-based auth, the Secret must be in the `certforge-system` namespace (or `secretNamespace` if overridden).

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

  # Option A: Secret-based auth
  # Secret must be in certforge-system (or secretNamespace)
  authSecretRef:
    name: certforge-credentials

  # Option B: Workload Identity (no Secret needed)
  # workloadIdentity:
  #   audience: https://app.certgov.app

  # Optional
  issuanceProfileID: ""
  secretNamespace: ""      # CertForgeClusterIssuer only
```

```bash theme={null}
kubectl get certforgeclusterissuer certforge -o yaml
```

***

## Authentication

### Secret-based (default)

The controller reads a long-lived API token from a Kubernetes Secret. Create the token under **Settings → API Keys** in the CertForge dashboard (needs `read` and `enroll` scopes).

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

# Rotate (controller re-reads on next reconcile — no restart needed)
kubectl create secret generic certforge-credentials \
  --namespace certforge-system \
  --from-literal=token=<new-api-token> \
  --dry-run=client -o yaml | kubectl apply -f -
```

For `CertForgeIssuer` (namespaced), create the Secret in the issuer's namespace:

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

### Workload Identity (recommended for production)

Workload Identity lets the controller authenticate using a short-lived [projected ServiceAccount token](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection) instead of a long-lived API key. The kubelet writes and rotates the token automatically — no Kubernetes Secret is required.

**How it works:**

1. The Helm chart adds a projected `serviceAccountToken` volume to the controller pod.
2. The kubelet writes a signed OIDC JWT to `/var/run/secrets/certforge/token`, bound to the configured audience.
3. The controller re-reads this file on every API call — token rotation is fully transparent.
4. CertForge validates the JWT against the cluster's OIDC JWKS endpoint and grants the permissions configured in the matching Workload Identity Provider.

**Step 1 — Find your cluster's OIDC issuer URL:**

```bash theme={null}
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
```

| Cluster type   | Typical issuer URL                                                            |
| -------------- | ----------------------------------------------------------------------------- |
| EKS            | `https://oidc.eks.<region>.amazonaws.com/id/<cluster-id>`                     |
| GKE            | `https://container.googleapis.com/v1/projects/<p>/locations/<l>/clusters/<c>` |
| AKS            | `https://<uuid>.hcp.<region>.azmk8s.io`                                       |
| kubeadm / kind | `https://kubernetes.default.svc`                                              |

**Step 2 — Configure a Workload Identity Provider in CertForge:**

Go to **Settings → Workload Identity → Add Provider**:

| Field            | Value                                                     |
| ---------------- | --------------------------------------------------------- |
| Name             | `certforge-issuer (production)`                           |
| OIDC Issuer URL  | Paste the URL from Step 1                                 |
| Audience         | `https://app.certgov.app` (or your EU URL)                |
| Allowed Subjects | `system:serviceaccount:certforge-system:certforge-issuer` |
| Scopes           | `read`, `enroll`                                          |

<Tip>
  Use an exact subject for production. The `system:serviceaccount:certforge-system:certforge-issuer`
  format identifies the precise ServiceAccount — no other workload can impersonate it.
  Trailing `*` wildcards are supported (e.g. `system:serviceaccount:certforge-system:*`) but
  should only be used in dev/staging environments.
</Tip>

**Step 3 — Install or upgrade with workload identity enabled:**

```bash theme={null}
helm upgrade --install certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --create-namespace \
  --set certforge.url=https://app.certgov.app \
  --set tokenSecret.create=false \
  --set workloadIdentity.enabled=true \
  --set workloadIdentity.audience=https://app.certgov.app
```

**Step 4 — Update your issuer spec (omit `authSecretRef`):**

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

**Step 5 — Verify:**

```bash theme={null}
# Issuer should reach Ready=True without a Secret
kubectl get certforgeclusterissuer certforge -o jsonpath='{.status.conditions[?(@.type=="Ready")].message}'
# Expected: "Credentials verified, connected to https://app.certgov.app"

# Confirm no Secret is referenced
kubectl get certforgeclusterissuer certforge -o jsonpath='{.spec.authSecretRef}' # should be empty
```

**Migrating from Secret-based auth:**

1. Add the Workload Identity Provider in CertForge (Step 2 above).
2. Upgrade the Helm chart with `workloadIdentity.enabled=true` and `tokenSecret.create=false`.
3. Update the issuer spec to use `workloadIdentity` instead of `authSecretRef`.
4. Once `Ready=True` is confirmed, delete the old Secret.

```bash theme={null}
kubectl delete secret certforge-credentials --namespace certforge-system
```

***

## RBAC

The controller's service account is granted the following permissions by the Helm chart:

| Resource                                                    | Verbs                                                                   |
| ----------------------------------------------------------- | ----------------------------------------------------------------------- |
| `certificaterequests`                                       | get, list, watch, update, patch                                         |
| `certificaterequests/status`                                | update, patch                                                           |
| `certificates`                                              | get, patch                                                              |
| `signers` (certforge.io/\*)                                 | approve                                                                 |
| `certforgeissuers`, `certforgeclusterissuers`               | get, list, watch                                                        |
| `certforgeissuers/status`, `certforgeclusterissuers/status` | update, patch                                                           |
| `secrets`                                                   | get (in issuer namespaces only)                                         |
| `leases`                                                    | get, list, watch, create, update, patch, delete (certforge-system only) |

No cluster-wide Secret read access is granted.

***

## Security hardening

### NetworkPolicy

Restrict the controller pod to only the traffic it actually needs. Enable with `networkPolicy.enabled=true` in your values. Requires a CNI with NetworkPolicy support (Calico, Cilium, etc.).

When enabled, the policy allows:

| Direction | Ports                                  | Purpose                                                  |
| --------- | -------------------------------------- | -------------------------------------------------------- |
| Egress    | UDP/TCP 53                             | DNS resolution                                           |
| Egress    | TCP 443                                | Kubernetes API server (managed clusters) + CertForge API |
| Egress    | TCP `kubeApiServerPort` (default 6443) | Kubernetes API server (kubeadm clusters)                 |
| Ingress   | TCP 8080                               | Prometheus metrics scrape                                |
| Ingress   | TCP 8081                               | Kubelet health probes                                    |

```yaml theme={null}
networkPolicy:
  enabled: true
  kubeApiServerPort: 443   # for EKS/GKE/AKS; keep 6443 for kubeadm
```

### Prometheus metrics

Enable a `ServiceMonitor` for Prometheus Operator integration:

```yaml theme={null}
serviceMonitor:
  enabled: true
  interval: 30s
  scrapeTimeout: 10s
  additionalLabels:
    release: prometheus   # match your Prometheus Operator's selector
```

The controller exposes standard controller-runtime metrics on `:8080/metrics`.

***

## Controller logs

The controller writes structured JSON logs:

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

Common log entries:

| Message                                              | Meaning                                                                    |
| ---------------------------------------------------- | -------------------------------------------------------------------------- |
| `issuer ready`                                       | Controller connected to CertForge; issuer marked Ready                     |
| `submitted to CertForge`                             | CSR sent; waiting for approval                                             |
| `certificate issued`                                 | CertForge issued the cert; written to CertificateRequest                   |
| `request pending approval`                           | In the human-approval queue — check the CertForge dashboard                |
| `certificate request denied by CertForge approver`   | Approver rejected; CR marked Denied (terminal)                             |
| `CertForge ping failed`                              | Auth or connectivity failure; issuer set to Ready=False and retried in 30s |
| `propagating denial from sibling CertificateRequest` | cert-manager retry blocked — previous request was denied                   |

***

## Upgrading

```bash theme={null}
helm upgrade certforge-issuer oci://ghcr.io/certforge-llc/charts/certforge-issuer \
  --namespace certforge-system \
  --reuse-values
```

The controller performs a rolling update. In-flight certificate requests are not lost — the controller picks up any pending request IDs from CertificateRequest annotations on restart.

### v0.1.x → v0.2.x

`authSecretRef` is now an optional pointer field (previously required). Existing `authSecretRef`-based issuers continue to work without changes. The new `workloadIdentity` field is the alternative.

***

## High availability

```yaml theme={null}
replicaCount: 2
leaderElect: true
```

Only one replica processes requests at a time; the others stand by. All replicas must reach the CertForge API. The leader election lease lives in `certforge-system`.

***

## Uninstall

```bash theme={null}
helm uninstall certforge-issuer --namespace certforge-system
kubectl delete namespace certforge-system
# Remove CRDs only if you no longer need them
kubectl delete crd certforgeissuers.certforge.io certforgeclusterissuers.certforge.io
```

Uninstalling does not delete `CertificateRequest` objects or Kubernetes Secrets containing issued certificates — those remain intact.
