A Kubernetes controller that syncs TLS certificates from Kubernetes Secrets into AWS Certificate Manager (ACM).
cert-manager handles issuance and renewal of TLS certificates into Kubernetes Secrets. But AWS load balancers, CloudFront distributions, and API Gateway custom domains reference certificates by ACM ARN — not by Kubernetes Secret. Without automation, every cert renewal requires a manual ACM import or a Terraform apply.
acm-sync watches annotated Secrets and keeps ACM in sync. When cert-manager rotates a cert, the controller imports the new material into the existing ACM ARN, so downstream AWS resources pick up the new cert with no infrastructure changes.
The controller watches Secret resources cluster-wide. Secrets opt in via an annotation:
apiVersion: v1
kind: Secret
metadata:
name: api-tls
namespace: production
annotations:
acm-sync.maplarge.com/enabled: "true"
acm-sync.maplarge.com/region: "us-east-1"
acm-sync.maplarge.com/arn: "arn:aws:acm:us-east-1:123456789012:certificate/abcd-1234"
acm-sync.maplarge.com/tags: "env=prod,team=platform"
type: kubernetes.io/tls
data:
tls.crt: ...
tls.key: ...When the Secret changes, the controller:
- Detects material change via SHA256 of
tls.crt. - Calls
acm:ImportCertificateagainst the ARN in the annotation (in-place update). - Applies tags and updates status annotations on the Secret.
If the arn annotation is absent, the controller imports a new certificate and writes the returned ARN back to the Secret. This keeps bootstrap simple — create the Secret, and the ARN lands on it automatically.
| Annotation | Required | Description |
|---|---|---|
acm-sync.maplarge.com/enabled |
yes | Must be "true" to opt in |
acm-sync.maplarge.com/region |
yes | Target AWS region (comma-separated for multi-region) |
acm-sync.maplarge.com/arn |
no | Existing ACM ARN; if absent, a new cert is imported |
acm-sync.maplarge.com/tags |
no | Comma-separated key=value pairs |
| Annotation | Description |
|---|---|
acm-sync.maplarge.com/last-synced-arn |
ARN written on the most recent successful sync |
acm-sync.maplarge.com/last-synced-time |
RFC3339 timestamp of last successful sync |
acm-sync.maplarge.com/last-synced-hash |
SHA256 of tls.crt from last sync |
acm-sync.maplarge.com/last-error |
Most recent error; cleared on success |
- Kubernetes 1.28+
- cert-manager installed (or any other source writing
kubernetes.io/tlsSecrets) - AWS credentials available to the controller via any method supported by the SDK v2 default credential chain:
- EKS Pod Identity (recommended for EKS)
- IRSA (IAM Roles for Service Accounts, for EKS with OIDC)
- Environment variables (
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, for non-EKS clusters) - EC2 instance metadata (IMDS, for self-managed clusters on EC2)
- Shared credentials file (for local development)
The Helm chart is published as an OCI artifact to GitHub Container Registry:
helm install acm-sync oci://ghcr.io/maplarge/charts/acm-sync \
--namespace acm-sync \
--create-namespaceTo install a specific version:
helm install acm-sync oci://ghcr.io/maplarge/charts/acm-sync --version 1.2.3 \
--namespace acm-sync \
--create-namespaceSee charts/acm-sync/README.md for the full values reference, partition-specific examples (commercial vs. GovCloud), FIPS configuration, and authentication setup for EKS Pod Identity, IRSA, and environment variables.
The controller's IAM role requires:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"acm:ImportCertificate",
"acm:AddTagsToCertificate",
"acm:ListTagsForCertificate",
"acm:DescribeCertificate"
],
"Resource": "*"
}
]
}Resource: "*" is required for ImportCertificate without a pre-existing ARN. Once all certs have stable ARNs, you can scope this down to specific ARN prefixes.
The controller uses the AWS SDK v2 default credential chain, which tries the following sources in order:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY, and optionallyAWS_SESSION_TOKEN) - Shared credentials file (
~/.aws/credentials) - EKS Pod Identity (via the Pod Identity Agent injecting container credentials)
- IRSA (via
AWS_WEB_IDENTITY_TOKEN_FILEandAWS_ROLE_ARNprojected by EKS) - EC2 instance metadata (IMDS)
No code or configuration changes are needed to switch between methods — the SDK resolves credentials automatically. For production EKS deployments, Pod Identity or IRSA are recommended. Environment variables can be useful for local development or non-EKS clusters.
Set credentials directly on the controller pod (e.g., via a Kubernetes Secret mounted as env vars):
helm install acm-sync ./charts/acm-sync \
--namespace acm-sync --create-namespace \
--set-json 'extraEnv=[{"name":"AWS_ACCESS_KEY_ID","valueFrom":{"secretKeyRef":{"name":"aws-creds","key":"access-key-id"}}},{"name":"AWS_SECRET_ACCESS_KEY","valueFrom":{"secretKeyRef":{"name":"aws-creds","key":"secret-access-key"}}}]'This is the simplest approach for non-EKS clusters or local testing, but requires managing credential rotation yourself.
EKS Pod Identity is the newer, simpler approach. It doesn't require an OIDC provider or ServiceAccount annotations — you create an association between the IAM role and the Kubernetes ServiceAccount directly via the EKS API.
- Create the IAM role with the ACM permissions above and a trust policy for the EKS Pod Identity service principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
]
}
]
}- Create the Pod Identity association:
aws eks create-pod-identity-association \
--cluster-name MY_CLUSTER \
--namespace acm-sync \
--service-account acm-sync \
--role-arn arn:aws:iam::123456789012:role/acm-sync- Install the chart with no ServiceAccount annotations needed:
helm install acm-sync ./charts/acm-sync \
--namespace acm-sync --create-namespaceFor clusters without Pod Identity support, or when using self-managed OIDC providers:
helm install acm-sync ./charts/acm-sync \
--namespace acm-sync --create-namespace \
--set serviceAccount.annotations."eks\.amazonaws\.com/role-arn"=arn:aws:iam::123456789012:role/acm-syncSee charts/acm-sync/README.md for OIDC trust policy examples for both commercial and GovCloud.
acm-sync runs in both AWS commercial and AWS GovCloud. Partition and endpoints are resolved automatically from the configured region — no partition-specific builds.
| Partition | Regions tested |
|---|---|
aws |
us-east-1, us-west-2 |
aws-us-gov |
us-gov-west-1 |
For FIPS-required deployments, set useFipsEndpoint: true in Helm values.
Prometheus metrics exposed on :8080/metrics:
acm_sync_reconcile_total{result="success|error"}acm_sync_reconcile_duration_secondsacm_sync_certificate_expiry_timestamp_seconds{secret,namespace,arn}acm_sync_last_sync_timestamp_seconds{secret,namespace,arn}
The controller emits Kubernetes events on each sync attempt. Use kubectl describe secret <name> to see recent activity.
Structured JSON logs with secret, namespace, region, and arn fields on every reconcile.
See CLAUDE.md for project conventions, then:
make helpCommon targets:
make build # Compile binary
make test # Unit tests
make lint # golangci-lint
make run # Run controller locally against current kubecontext
make e2e # E2E tests against LocalStack- Issuing certificates (use cert-manager).
- Managing ALB / CloudFront / API Gateway resources (use Terraform).
- Cross-partition sync (architecturally impossible).
- General-purpose AWS secret sync (use External Secrets Operator).
TBD — internal MapLarge project.