Skip to content

fix(security+k8s): retry cosign sign on Rekor 409 conflict; honor cloudExtras startupProbe + periodSeconds#316

Merged
Cre-eD merged 4 commits into
mainfrom
fix/sign-409-startup-probe
Jun 11, 2026
Merged

fix(security+k8s): retry cosign sign on Rekor 409 conflict; honor cloudExtras startupProbe + periodSeconds#316
Cre-eD merged 4 commits into
mainfrom
fix/sign-409-startup-probe

Conversation

@Cre-eD

@Cre-eD Cre-eD commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Two production-surfaced fixes, bundled per repo convention.

1. Signing: tolerate Rekor 409 createLogEntryConflict by retrying with a fresh signature

When several CI deploys run in parallel for stacks that share one image (one compose file, several runs: stacks), sc image sign can abort with:

signing bundle: error signing bundle: [POST /api/v1/log/entries][409] createLogEntryConflict
{"code":409,"message":"an equivalent entry already exists in the transparency log with UUID …"}

An identical entry already being in the tlog means the signature upload effectively succeeded — keyless ephemeral keys (and randomized-ECDSA key-based signatures) can't produce identical entries across independent invocations, so the typical cause is cosign re-uploading an entry whose first attempt timed out client-side but landed server-side. The image ends up signed, yet the deploy fails. Observed breaking every push-fanout deploy in a multi-stack repo; single-stack dispatches passed.

Fix: runCosignSign retries the full cosign sign invocation (up to 3 attempts) when — and only when — the failure is a Rekor entry conflict. A fresh invocation mints a fresh signature, so a retry cannot conflict with itself. Any other error still fails fast. Shared by the keyless and key-based signers.

2. Probes: cloudExtras.startupProbe was silently dropped; periodSeconds unsupported

k8s.CloudExtras/DeploymentConfig had readinessProbe + livenessProbe but no startupProbe field, so a user-supplied startup budget never reached the cluster — the container fell back to the readiness probe (or its 60s default TCP window) as its cold-start budget. On a freshly-provisioned Autopilot node (image pull + migrations + worker boot) that gets the pod SIGTERM-killed mid-boot, which converts an ordinary node move into an outage for single-replica services.

Additionally CloudRunProbe only understood the duration-typed interval key; configs written with the k8s-native periodSeconds spelling silently lost their probe period (plain integers don't survive the YAML→time.Duration conversion).

Fix:

  • CloudRunProbe.PeriodSeconds added; takes precedence over interval in toProbeArgs.
  • StartupProbe added to CloudExtras + DeploymentConfig + DeploymentArgs, threaded gke_autopilot → kube_run → deployment with the same ingress-container fallback semantics as readiness/liveness. Existing behavior (startup := readiness when nothing is specified) is unchanged.

Tests

  • TestIsRekorConflict (real cosign stderr shapes, incl. non-conflict 409s that must NOT match), TestKeylessSigner_Sign_RetriesOnRekorConflict, …_NoRetryOnOtherErrors, …_GivesUpAfterMaxConflictAttempts — via an execCommand seam, asserting exact call counts.
  • TestStartupProbeExtraction / TestProbePeriodSecondsExtraction / TestStartupProbeAbsentConvertDescriptor regression tests proving the yaml keys now survive the mapping (the original bug class).
  • TestToProbeArgs_PeriodSeconds — precedence table (periodSeconds > interval > unset).

go test ./pkg/security/signing/ ./pkg/clouds/k8s/ ./pkg/clouds/pulumi/kubernetes/ ./pkg/clouds/gcloud/ — all pass.

…udExtras startupProbe + periodSeconds

Two production-surfaced fixes:

1) Image signing: parallel CI deploys of stacks sharing one image can fail
   with Rekor "[POST /api/v1/log/entries][409] createLogEntryConflict" — an
   identical entry already exists, typically because cosign re-uploaded an
   entry whose first attempt timed out client-side but succeeded server-side.
   The signature is valid and present, yet the deploy aborts. Retry the full
   cosign invocation (fresh ephemeral / randomized-ECDSA signature cannot
   conflict with itself) up to 3 attempts; any non-conflict error still fails
   fast. Applied to both keyless and key-based signers via a shared helper.

2) Probes: cloudExtras.startupProbe was silently dropped — CloudExtras and
   DeploymentConfig had no StartupProbe field, so slow-booting services were
   stuck with the readiness probe (or its 60s default window) as their cold-
   start budget and got SIGTERM-killed mid-boot on fresh nodes. Add the field
   and thread it through gke_autopilot -> DeploymentConfig -> DeploymentArgs ->
   container spec with the same ingress-container fallback semantics as
   readiness/liveness. Also add the k8s-native periodSeconds spelling to
   CloudRunProbe (precedence over the legacy duration-typed interval, which
   does not survive YAML conversion of plain integers).

Tests: Rekor-conflict detection + retry/no-retry/give-up behavior via an exec
seam; ConvertDescriptor regression tests proving startupProbe/periodSeconds
now survive the yaml mapping; toProbeArgs periodSeconds precedence table.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

Semgrep Scan Results

Repository: api | Commit: 31fd55c

Check Status Details
✅ Semgrep Pass 0 total findings (no error/warning)

Scanned at 2026-06-10 13:57 UTC

@github-actions

github-actions Bot commented Jun 10, 2026

Copy link
Copy Markdown

Security Scan Results

Repository: api | Commit: 31fd55c

Check Status Details
✅ Secret Scan Pass No secrets detected
✅ Dependencies (Trivy) Pass 0 total (no critical/high)
✅ Dependencies (Grype) Pass 2 total (no critical/high)
📦 SBOM Generated 527 components (CycloneDX)

Scanned at 2026-06-10 13:57 UTC

Cre-eD added 2 commits June 10, 2026 13:50
…onds and Rekor-409 self-heal

- kube_run_probe_test.go converted from testify to gomega per docs/TESTING.md
  (gomega is the mandated framework; the neighboring testify file predates
  the policy).
- GKE Autopilot guide: startupProbe added to the Complete CloudExtras
  Reference yaml + field table. The reference already promised periodSeconds
  on probes before the code supported it — now both are true.
- Troubleshooting: createLogEntryConflict (Rekor 409) entry documenting the
  cause and the automatic fresh-signature retry.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
…honor explicit probes on multi-port containers; DI exec seam

Codex + Gemini review findings:

- gke_autopilot_stack.go forwards readiness/liveness into DeploymentArgs but
  not the new StartupProbe — the GKE Autopilot path (the primary consumer)
  silently dropped it, reintroducing the original bug one layer up. Wired.
- deployment.go required a single port / mainPort even for an EXPLICIT
  startup probe, dropping it on multi-port containers; readiness applies
  explicit probes unconditionally and toProbeArgs resolves the port from
  httpGet.port / mainPort / first port. Mirrored that behavior.
- Replaced the package-global execCommand test seam with an exec field on
  both signers (data race if tests ever parallelize; proper DI).
- Retry warning now goes to stderr (stdout can be piped/parsed) and no
  longer claims a "fresh signature" — key-based signing with deterministic
  keys (ed25519) reproduces the same signature; documented why exhausting
  the loop and failing is the correct outcome there (a tlog entry existing
  does not prove the signature reached the registry).
- IntervaSeconds typo field marked Deprecated.
- Tests: key-based 409 persistent-conflict (exhausts loop, surfaces error)
  and transient-conflict (one retry then success) cases added.

Declined (with rationale): applying the global startupProbe to non-ingress
containers — global probes are deliberately ingress-only, mirroring the
readiness/liveness semantics; HTTP probes on worker/sidecar containers is
the failure mode that guard exists to prevent.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@Cre-eD

Cre-eD commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Preview validation — PASS

Preview build v2026.6.3-pre.a59ed2e-preview.a59ed2e (run 27277300595) deployed a real cloud-compose client stack to a GKE Autopilot staging environment (action pinned by the tagged release commit SHA e781b89).

startupProbe / periodSeconds wiring (the silent-drop fix):

probe before (silently-applied defaults) after preview deploy cloudExtras yaml
startup failureThreshold=5, period=10 (60s budget) failureThreshold=10, period=15 (160s) ✅ exact match
readiness failureThreshold=5, period=10 failureThreshold=5, period=15 periodSeconds honored

Pod went 2/2 Ready, app serving 200 end-to-end after the deploy (one transient 521 during the normal Caddy aggregator rollout window).

Signing: the deploy's scan→sign→attest→verify chain completed successfully with the retry-wrapped sign path (no Rekor conflict occurred in this run; conflict behavior is covered by the unit suite — keyless retry-then-succeed, non-conflict fail-fast, bounded exhaustion, and the key-based deterministic-conflict cases).

smecsia
smecsia previously approved these changes Jun 10, 2026
universe-ops
universe-ops previously approved these changes Jun 10, 2026
govulncheck fails on GO-2026-5039 (net/textproto) + GO-2026-5037
(crypto/x509), both fixed in go1.26.4 — pre-existing on main (red since
2026-06-04), surfaced here because the gate blocks this PR. setup-go reads
go-version-file: go.mod, so the directive bump moves CI to the patched
toolchain. No toolchain line, no dependency changes.

Also trims multi-line comment blocks added by this PR down to one-liners.

Signed-off-by: Dmitrii Creed <creeed22@gmail.com>
@Cre-eD
Cre-eD dismissed stale reviews from universe-ops and smecsia via 49fb7db June 10, 2026 13:56
@Cre-eD
Cre-eD merged commit d5b826d into main Jun 11, 2026
21 checks passed
@Cre-eD
Cre-eD deleted the fix/sign-409-startup-probe branch June 11, 2026 09:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants