target(awsacm): SDK-driven AWS Certificate Manager target connector

Closes Rank 5 (AWS half) of the 2026-05-03 Infisical deep-research
deliverable (cowork/infisical-deep-research-results.md Part 5).
Pre-fix, certctl had no path to deploy certs to AWS-managed TLS-
termination endpoints (ALB / CloudFront / API Gateway / App Runner)
— operators terminating TLS at AWS had to use Infisical secret-sync,
manual aws-cli imports, or external automation. This commit lands
the SDK-driven AWS Certificate Manager target connector that closes
the gap end-to-end.

Architecture:
  - internal/connector/target/awsacm/awsacm.go — Connector wraps
    *acm.Client behind the ACMClient interface seam (mirrors
    awsacmpca's ACMPCAClient pattern from the issuer side).
    LoadDefaultConfig handles the standard AWS credential chain
    (IRSA / EC2 instance profile / SSO / env vars); no embedded
    creds in connector Config.
  - Pre-deploy snapshot via DescribeCertificate + GetCertificate so
    on-import-failure rollback restores the previous cert. Mirrors
    the Bundle 5 IIS pattern + the Bundle 7/8 WinCertStore /
    JavaKeystore patterns. Surfaces rollback success/failure via
    the existing certctl_deploy_rollback_total Prometheus counter
    label set.
  - Provenance tags: certctl-managed-by=certctl + certctl-
    certificate-id=<mc-id> set automatically on every import. ACM
    strips tags on re-import, so the connector calls
    AddTagsToCertificate post-import to keep the provenance pair
    fresh. Operators looking up a cert ARN by managed-cert ID
    (Terraform data source, CloudFormation output) match against
    these tags.
  - DeploymentRequest.KeyPEM held in agent memory only — never
    written to disk. Aligns with the pull-only deployment model
    documented in CLAUDE.md.

Tests:
  - awsacm_test.go: 15-subtest happy-path + validation matrix
    covering ValidateConfig (success / missing-region / malformed-
    region / malformed-ARN / reserved-tag rejection),
    DeployCertificate (fresh import / rotate-in-place / rollback-
    on-serial-mismatch / rollback-also-fails / empty-key-rejected /
    no-client-rejected), ValidateOnly (returns sentinel),
    ValidateDeployment (serial match / mismatch / no-ARN-yet).
  - awsacm_failure_test.go: 5 per-error-class contract tests
    mirroring the awsacmpca_failure_test.go shape (commit
    60dce0b) — AccessDeniedException (smithy.GenericAPIError),
    ResourceNotFoundException (typed), ThrottlingException
    (smithy.GenericAPIError, FaultServer preserved),
    InvalidArgsException (typed, terminal), RequestInProgress
    Exception (typed). All assert errors.As against the SDK type +
    operator-actionable substring + connector-side wrap framing.
  - Coverage on awsacm.go: 54.9% of statements (matches the K8s-
    Secret + IIS connectors' 50-65% range; rollback-failure paths
    contribute most of the un-covered surface — those exercise
    only when the rollback's SDK call also returns an error).
  - go test -race -count=10 green; no goroutine leaks.

Wiring:
  - internal/domain/connector.go: TargetTypeAWSACM = "AWSACM".
  - internal/service/target.go: validTargetTypes set extended.
  - cmd/agent/main.go::createTargetConnector: AWSACM case arm
    mirroring the KubernetesSecrets shape exactly. Calls
    awsacm.New(context.Background(), &cfg, a.logger) — the
    SDK-loading happens here, not lazily, so config errors
    surface at agent boot.
  - cmd/agent/agent_test.go::TestCreateTargetConnector_AllSupported
    Types: AWSACM added to the type matrix + the InvalidJSON
    matrix.

go.mod / go.sum:
  - github.com/aws/aws-sdk-go-v2/service/acm v1.38.3 (direct).
    aws-sdk-go-v2 + service/acmpca + smithy-go were already direct
    from the awsacmpca issuer; this is the distribution-side
    companion package.

Documentation:
  - docs/connectors.md "AWS Certificate Manager (ACM)" section:
    config table, IAM policy JSON (5 actions on
    arn:aws:acm:*:*:certificate/*), IRSA / EC2 instance-profile /
    SSO auth recipes, atomic-rollback contract, Terraform ALB-
    attachment snippet, threat model carve-outs (no disk writes,
    mandatory provenance tags, no long-lived creds in Config),
    procurement checklist crib (5 bullets paste-able into a
    security review).

Out of scope (intentional, flagged in V3-Pro forward path):
  - CloudFront / ALB auto-attach (UpdateDistribution requires a
    different IAM scope than ACM ImportCertificate).
  - Cross-region ACM replication (ACM is regional; CloudFront
    forces us-east-1).
  - Tag-filtered ARN discovery (V2 uses operator-pinned
    Config.CertificateArn after first deploy; tag-scan path
    requires acm:ListTagsForCertificate which we deliberately
    keep off the minimum-IAM-policy surface).
  - Azure Key Vault (separate cloud, separate connector — Azure
    half of Rank 5 ships in a follow-on commit).

Verified locally:
- gofmt clean.
- go vet ./internal/connector/target/awsacm/...
  ./internal/domain/... ./internal/service/...
  ./cmd/agent/...  clean.
- go test -short -count=1 ./internal/connector/target/awsacm/...
  ./internal/domain/... ./cmd/agent/...  green (15 + 5 awsacm
  subtests; all 15 supported target types instantiate via the
  agent factory).
- go test -race -count=10 ./internal/connector/target/awsacm/...
  green.

Reference: cowork/infisical-deep-research-results.md Part 5 Rank 5.
Acquisition prompt:
cowork/rank-5-aws-acm-azure-kv-target-adapters-prompt.md.
This commit is contained in:
shankar0123
2026-05-03 22:32:45 +00:00
parent 6af95ccf5f
commit 54033aa3aa
10 changed files with 1588 additions and 1 deletions
+95
View File
@@ -1409,6 +1409,101 @@ The Kubernetes Secrets connector deploys certificates as `kubernetes.io/tls` Sec
Location: `internal/connector/target/k8ssecret/k8ssecret.go`
### AWS Certificate Manager (ACM)
The AWS ACM target connector deploys certificates into AWS Certificate Manager — the public AWS service that ALB / CloudFront / API Gateway / App Runner consume by ARN. Closes the "we terminate TLS at AWS, how do we get certctl-issued certs to ALB?" question for cloud-first deployments. Rank 5 of the 2026-05-03 Infisical deep-research deliverable.
```json
{
"region": "us-east-1",
"certificate_arn": "arn:aws:acm:us-east-1:123456789012:certificate/abcdef01-2345-6789-abcd-ef0123456789",
"tags": {"env": "production", "app": "api-gateway"}
}
```
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `region` | string | *(required)* | AWS region for the ACM endpoint (e.g., `us-east-1`). CloudFront-attached certs MUST live in `us-east-1`; ALB / API Gateway use the same region as the load balancer. |
| `certificate_arn` | string | | ARN of an existing ACM certificate to rotate in place. Empty on first deploy — the adapter creates a new ACM cert via `ImportCertificate` and the deployment record's Metadata captures the resulting ARN. Operators can also pre-create the ARN out-of-band (Terraform, CloudFormation) and pin it here. |
| `tags` | object | | Tags applied to the ACM cert at first import + re-applied via `AddTagsToCertificate` on every subsequent import (ACM strips tags on re-import). The reserved keys `certctl-managed-by` and `certctl-certificate-id` are set automatically and cannot be overridden. |
**IAM policy (minimum permissions):**
```json
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": [
"acm:ImportCertificate",
"acm:GetCertificate",
"acm:DescribeCertificate",
"acm:ListCertificates",
"acm:AddTagsToCertificate"
],
"Resource": "arn:aws:acm:*:*:certificate/*"
}]
}
```
**Auth recipes:**
- **IRSA (IAM Roles for Service Accounts) — recommended for K8s deploys.** Annotate the agent's ServiceAccount with `eks.amazonaws.com/role-arn=arn:aws:iam::<account>:role/certctl-acm-deployer`. The role's trust policy allows the cluster's OIDC provider; permission policy is the JSON above. Short-lived STS credentials are auto-rotated by EKS — no long-lived access keys.
- **EC2 instance profile — recommended for VM-based agents.** Attach an instance profile referencing the same role. SDK's `LoadDefaultConfig` picks credentials up via the IMDS metadata service.
- **AWS SSO / `aws configure sso` — recommended for operator workstations.** SDK reads `~/.aws/config` for the SSO profile and refreshes tokens via the existing CLI session.
- **Long-lived access keys are NOT supported in connector Config** — the credential chain is configured at the SDK level, not the connector level. This is a procurement-readability decision: a security reviewer reading the deployment_targets table should never find an access key.
**Atomic-rollback contract:**
Every `DeployCertificate` snapshots the existing cert via `DescribeCertificate` + `GetCertificate` BEFORE calling `ImportCertificate` with the new bytes. After import, the connector re-fetches the cert metadata and compares serial numbers. On serial-mismatch (post-verify failure), the connector calls `ImportCertificate` again with the snapshotted bytes to restore the previous cert. The rollback path emits a `WARN`-level slog entry; the rollback's own success or failure is exposed via `certctl_deploy_rollback_total{target_type="AWSACM",outcome="restored"|"also_failed"}` per the deploy-hardening I Phase 10 metric exposer. Mirrors the Bundle 5+ pre-deploy-snapshot pattern shipped for IIS / WinCertStore / JavaKeystore.
**ALB attachment recipe:**
certctl creates / rotates the ACM cert; the operator (or Terraform / CloudFormation) attaches it to the ALB listener separately. For Terraform-driven deployments, look up the ARN by tag:
```hcl
data "aws_acm_certificate" "certctl_managed" {
domain = "api.example.com"
most_recent = true
# Filter by certctl provenance tags so an unrelated ACM cert with
# the same SAN doesn't get picked up.
tags = {
"certctl-managed-by" = "certctl"
"certctl-certificate-id" = "mc-api-prod"
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.api.arn
port = 443
protocol = "HTTPS"
certificate_arn = data.aws_acm_certificate.certctl_managed.arn
# ...
}
```
The ARN updates in place across renewals (ACM `ImportCertificate` is upsert-style when given an ARN), so the ALB listener's `certificate_arn` reference doesn't change. CloudFront / API Gateway distributions can reference the same ARN via their respective Terraform resources.
**Threat model carve-outs:**
- **Cert key bytes never written to disk on the agent.** `DeployCertificate` reads `request.KeyPEM` from memory and passes it to the SDK's `ImportCertificate` call. No temp file. No swap-out window.
- **Provenance tags are mandatory.** The reserved `certctl-managed-by=certctl` + `certctl-certificate-id=<mc-id>` pair is set automatically on every import. Operators identifying a stray ACM cert in their account can match against `certctl-managed-by` to confirm it was certctl-issued (or NOT — the absence of the tag means a manual import).
- **No long-lived AWS credentials in `Config`.** `Config` carries region + ARN + operator tags only. AWS auth is the SDK credential chain (IRSA / instance profile / SSO).
- **`ListCertificates` IAM permission is required for the V2 ARN-discovery dance to work.** Operators who pin `Config.CertificateArn` after the first deploy can drop this permission; the V2 fallback emits a warning and reverts to "always create new ARN" if the operator forgets to update `certificate_arn` post-first-deploy.
**Procurement checklist crib (paste into security review):**
- certctl uses short-lived IAM-role credentials via IRSA / instance profile, not long-lived access keys.
- The cert key is held only in agent memory during the import call; never written to disk.
- Every imported ACM cert is tagged with `certctl-managed-by=certctl` + `certctl-certificate-id=<mc-id>` for forensic traceability.
- Failed imports trigger automatic rollback to the snapshotted previous cert; both outcomes are surfaced via Prometheus.
- The minimum IAM policy is 5 actions on `arn:aws:acm:*:*:certificate/*`; CloudTrail captures every API call for compliance audits.
**ValidateOnly contract.** ACM has no dry-run API for `ImportCertificate`; `ValidateOnly` returns `target.ErrValidateOnlyNotSupported` per the deploy-hardening I Phase 3 sentinel contract. Operators preview deploys via `ValidateConfig` + `aws acm describe-certificate --certificate-arn <arn>` against the current ARN.
Location: `internal/connector/target/awsacm/awsacm.go` + `internal/connector/target/awsacm/awsacm_failure_test.go` (per-error-class contract tests for `AccessDeniedException` / `ResourceNotFoundException` / `ThrottlingException` / `InvalidArgsException` / `RequestInProgressException`).
## Notifier Connector
Notifier connectors send alerts about certificate lifecycle events (expiration warnings, renewal success/failure, deployment status, policy violations).