A production-grade, highly extensible Kubernetes operator that watches TLS Secrets (kubernetes.io/tls) and automatically replicates them to GCP Certificate Manager as SELF_MANAGED certificates.
This controller implements a "push-based" model to bridge asynchronous TLS generation (e.g., via cert-manager) to GCP HTTPS Load Balancers, avoiding the need for Terraform or other orchestration tools to block on asynchronous certificate provisioning.
graph TD
A[cert-manager] -->|Generates Certificate| B(K8s TLS Secret)
B -->|Watch Events| C[k8s-gcp-cert-injector]
C -->|Validate & Standardize Name| D{Meets GCP 63-char Limits?}
D -->|Yes| E[Push to GCP Certificate Manager]
D -->|No - Error| F[Log Warning & Update Status]
E -->|Success| G[Inject cert-injector.io/finalizer]
B -->|Deleted| H[Cleanup Finalizer]
H -->|Trigger Hook| I[Delete from GCP Certificate Manager]
To safely push certificates to GCP, the operator requires permissions on the GCP Certificate Manager API. It is highly recommended to authenticate using GKE Workload Identity Federation rather than long-lived static service account keys.
To manage certificates, grant the GCP Service Account (GSA) the following predefined role:
roles/certificatemanager.editor(Certificate Manager Editor)
If your organization requires the principle of least privilege, you can define a custom IAM role with the following granular permissions:
certificatemanager.certificates.createcertificatemanager.certificates.getcertificatemanager.certificates.deletecertificatemanager.certificates.updatecertificatemanager.certificates.list
Follow these steps to configure Workload Identity on GKE to securely authorize the operator:
export PROJECT_ID="your-gcp-project-id"
export GSA_NAME="k8s-gcp-cert-injector"
export KSA_NAME="k8s-gcp-cert-injector-controller-manager"
export NAMESPACE="cert-injector-system"gcloud iam service-accounts create ${GSA_NAME} \
--project=${PROJECT_ID} \
--description="Service account for k8s-gcp-cert-injector operator" \
--display-name="k8s-gcp-cert-injector"gcloud projects add-iam-policy-binding ${PROJECT_ID} \
--member="serviceAccount:${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="roles/certificatemanager.editor"Allow the GKE-specific ServiceAccount to act as the GSA:
gcloud iam service-accounts add-iam-policy-binding \
${GSA_NAME}@${PROJECT_ID}.iam.gserviceaccount.com \
--project=${PROJECT_ID} \
--role="roles/iam.workloadIdentityUser" \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[${NAMESPACE}/${KSA_NAME}]"The operator is distributed as a Helm chart located under the charts/chart/ directory.
Prepare your values.yaml overrides or set values via --set:
# my-values.yaml
serviceAccount:
annotations:
iam.gke.io/gcp-service-account: "k8s-gcp-cert-injector@your-gcp-project-id.iam.gserviceaccount.com"
manager:
args:
- --leader-elect
- --gcp-project-id=your-gcp-project-id # Optional, falls back to metadata serverInstall the chart:
helm install k8s-gcp-cert-injector ./charts/chart \
--namespace cert-injector-system \
--create-namespace \
-f my-values.yamlTo sync a Kubernetes kubernetes.io/tls secret to GCP Certificate Manager, configure the secret with the annotations described below.
If you are using cert-manager to automatically provision and rotate TLS certificates, use the secretTemplate field to automatically inject the required sync annotations onto the generated Kubernetes Secret:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-example-com
namespace: prod
spec:
secretName: wild-example-com-tls
dnsNames:
- "*.example.com"
- "example.com"
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
secretTemplate:
annotations:
# Enable replication to GCP Certificate Manager
cert-injector.io/sync: "true"
# Optional: Override the certificate resource name in GCP
cert-injector.io/cert-name: "wildcard-example-com"
# Optional: Route this certificate to a remote/different GCP Project
cert-injector.io/gcp-project: "remote-gcp-project-id"
# Optional: Route the GCP SDK client to a specific Sovereign Cloud or Private Universe Domain (defaults to googleapis.com)
cert-injector.io/universe-domain: "my-universe.com"apiVersion: v1
kind: Secret
metadata:
name: wild-example-com-tls
namespace: prod
annotations:
# 1. Enable replication to GCP Certificate Manager
cert-injector.io/sync: "true"
# 2. Optional: override the resulting certificate name in GCP
# If not set, defaults to: k8s-cert-prod-wild-example-com-tls
cert-injector.io/cert-name: "wildcard-example-com"
# 3. Optional: replicate to a completely separate GCP project
cert-injector.io/gcp-project: "remote-gcp-project-id"
# 4. Optional: Route via a specific Sovereign/Private Universe Domain (defaults to googleapis.com)
cert-injector.io/universe-domain: "my-universe.com"
type: kubernetes.io/tls
data:
tls.crt: <base64-encoded-certificate-chain>
tls.key: <base64-encoded-private-key>| Annotation | Type | Description |
|---|---|---|
cert-injector.io/sync |
string ("true") |
Triggers replication to GCP Certificate Manager. |
cert-injector.io/cert-name |
string |
Custom name override for the GCP Certificate. Must be DNS-1123 compliant and |
cert-injector.io/gcp-project |
string |
Optional. Replicates this certificate to a completely different / remote GCP Project ID. |
cert-injector.io/universe-domain |
string |
Optional. Configures the GCP API base endpoint domain used by the Google Cloud SDK client to communicate (defaults to googleapis.com). Necessary for Sovereign or Private Clouds. Does not modify or suffix the certificate name. |
GCP Certificate Manager enforces a strict 63-character limit and a DNS-1123 lowercase alphanumeric format (^[a-z0-9-]{1,63}$) on certificate resource names.
- Fallback Format:
k8s-cert-{namespace}-{secret-name} - Pre-validation: If the fallback or custom override name exceeds 63 characters, or contains invalid characters, the controller logs an early, clear warning and does not send the request to GCP, preventing API-level reject loops.
This project uses a reproducible Nix developer environment managed via devenv.
Ensure you have devenv installed on your machine.
All Go commands should be run within the devenv shell to ensure consistent dependencies (Go 1.26.2, Kubebuilder CLI, controller-gen, etc.):
devenv shellThe integration test suite utilizes envtest to run against a real local Kubernetes Control Plane:
# Prefix with GOWORK=off to ignore outer Go workspaces during sandbox testing
GOWORK=off devenv shell make testFormat the codebase before pushing changes:
GOWORK=off devenv shell make fmt
GOWORK=off devenv shell make vetCopyright 2026. Licensed under the Apache License, Version 2.0.