Kubernetes & cert-manager
The kubernetes driver — the default — talks to the Kubernetes API directly over Laravel’s HTTP client, with no SDK. It provisions by adding a TLS entry and host rules to an Ingress, lets cert-manager issue the certificate into a secret, and reads cert-manager Certificate resources for status and listing:
CERTIFICATES_DRIVER=kubernetes
CERTIFICATES_K8S_NAMESPACE=tenants
CERTIFICATES_K8S_ISSUER=letsencrypt
CERTIFICATES_K8S_ISSUER_KIND=ClusterIssuer
CERTIFICATES_K8S_INGRESS_NAME=tenant-domains
CERTIFICATES_K8S_INGRESS_CLASS=nginx
CERTIFICATES_K8S_SERVICE_NAME=web
CERTIFICATES_K8S_SERVICE_PORT=80use RoundlyConsulting\Certificates\Facades\Certificates;
$certificate = Certificates::for('shop.example.com')->using('kubernetes')->issue();
Certificates::statusReport('shop.example.com', driver: 'kubernetes'); // Ready condition + notAftercert-manager issues asynchronously, so a first issue() patches the Ingress and returns the row still requested — cert-manager has not created or finished the Certificate yet. Run certificates:sync (or Certificates::sync('kubernetes')) to record the outcome once it is Ready.
Driver keys
| Key | Env | Default | Purpose |
|---|---|---|---|
base_url | CERTIFICATES_K8S_BASE_URL | https://kubernetes.default.svc | Kubernetes API server URL. |
token | CERTIFICATES_K8S_TOKEN | null | Bearer token; wins over token_path. |
token_path | CERTIFICATES_K8S_TOKEN_PATH | in-cluster SA token | File the token is read from when token is empty. |
ca_path | CERTIFICATES_K8S_CA_PATH | in-cluster SA CA | CA bundle for the API server’s TLS; null or empty uses the system bundle, only false (0, off, no) disables verification. |
namespace | CERTIFICATES_K8S_NAMESPACE | default | Namespace of the Ingress and the Certificate resources — for every certificate of this driver. |
issuer | CERTIFICATES_K8S_ISSUER | letsencrypt | cert-manager issuer name — for every certificate of this driver. |
issuer_kind | CERTIFICATES_K8S_ISSUER_KIND | ClusterIssuer | ClusterIssuer or Issuer — picks the annotation. |
ingress.name | CERTIFICATES_K8S_INGRESS_NAME | null | The Ingress to manage — set this. |
ingress.class | CERTIFICATES_K8S_INGRESS_CLASS | nginx | Ingress class annotation on a newly created Ingress. |
service.name | CERTIFICATES_K8S_SERVICE_NAME | null | Backend service routed for new hosts. |
service.port | CERTIFICATES_K8S_SERVICE_PORT | 80 | Backend service port. |
The issuer and namespace apply to every certificate of the driver. For a second issuer or namespace, register another driver with Certificates::extend() and pick it with using().
Authentication
By default the driver authenticates with the in-cluster service-account token and verifies the API server against the in-cluster CA bundle. From outside the cluster, set base_url and pass the token directly — an inline token wins over token_path — along with a CA bundle path:
CERTIFICATES_K8S_BASE_URL=https://k8s.example.com:6443
CERTIFICATES_K8S_TOKEN=service-account-token
CERTIFICATES_K8S_CA_PATH=/etc/ssl/k8s/ca.crtca_path is the CA bundle the API server’s certificate is verified against. null (or empty) verifies against the system CA bundle instead, and only a false value (CERTIFICATES_K8S_CA_PATH=false, or 0, off, no) disables TLS verification — not recommended. API errors and misconfiguration throw KubernetesApiException with the HTTP status and response body.
What the Ingress looks like
When the Ingress named by ingress.name does not exist yet, it is created with the ingress class and cert-manager issuer annotations — cert-manager.io/cluster-issuer for a ClusterIssuer, cert-manager.io/issuer otherwise. Each issuance appends a TLS entry whose secretName is the derived certificate name, plus one rule per host routing / to the configured service:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tenant-domains # ingress.name
namespace: tenants # namespace
annotations:
kubernetes.io/ingress.class: nginx # ingress.class
cert-manager.io/cluster-issuer: letsencrypt # issuer + issuer_kind
spec:
tls:
- hosts:
- shop.example.com
secretName: generated-tls-shop-example-com
rules:
- host: shop.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web # service.name
port:
number: 80 # service.portAn existing Ingress is merge-patched and keeps its own annotations, so make sure it already carries the cert-manager issuer annotation. If the Ingress already has a TLS entry for the secret, nothing changes — cert-manager keeps renewing the secret itself.
Status and listing
Status comes from the Certificate resource of the same name, mapped onto the registry lifecycle:
- Ready=True — issued, or expired once status.notAfter has passed.
- Issuing=True — pending: still being issued.
- Issuing=False with reason Failed, or a recorded lastFailureTime — failed.
- Anything else — expired when status.notAfter has passed, otherwise pending: Ready=False alone (cert-manager reports it while still issuing), no conditions yet, or no Certificate resource at all (ingress-shim creates it moments after the Ingress is patched). Pending is not an error.
expiresAt is status.notAfter and issuer is spec.issuerRef.name. get() lists every Certificate in the namespace by its first dnsName (or commonName) — run certificates:sync --driver=kubernetes to pull them into the registry. A pending report never demotes a row that is already requested or renewing.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.