You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
feat: add artifact signing behind experimental build tag - #1044
Adds opt-in artifact signing behind the experimental_signing Go build tag. Normal builds retain the baseline implementations and exclude the new signing packages. Build with make binary BUILD_TAGS=experimental_signing or pass the same build argument to the Docker build.
The experimental build supports signed ContainerProfile bundles, Rules and rule bindings; root-signed trust policies with guarded reload; signed config and clusterData; /policyz; and the sign-object CLI. Profile verification runs before grouped-section flattening, restores verified embedded content, rejects malformed embedded specs in either mode, and retries refused profiles after re-signing. Bundle assembly explicitly rejects grouped fragments; standalone grouped profiles remain supported. Binding selectors and rules come from authenticated signed content.
Passed locally after rebasing onto upstream c7a77c13 (Go 1.27):
Targeted config, health, cache, rule admission/binding/creator/types, storage, and signing tests in both applicable build variants.
Agent builds with and without the tag; signing CLI build.
Vet for the affected packages in both variants.
Race tests for the experimental profile cache, rule binding cache, rule creator, and rules watcher.
Default dependency graph excludes the experimental signing packages, Cosign, and Fulcio.
Adds CI coverage for both variants and build-isolation checks. Docker images and cluster/component tests have not been run.
Integration and limitations
Rebased onto current upstream c7a77c13. The branch contains the experimental signing implementation and its regression fixes; fork-only CI commits are excluded. The rebase preserves upstream host-sensor validation, dependency versions, canonical rule schemas, and watcher validation. It reuses upstream compact YAML and profile-list methods instead of adding duplicates. A regression test covers alert-to-enforce policy reload when upstream's checksum fetch optimization is active.
Flat profiles retain the legacy verification behavior, which accepts self-signed certificates and detects content changes without bundle-class signer authorization. Signed bundles with a real trust root are required when signer authorization is needed. Invalid boot trust policies leave signing disabled until a valid policy is loaded; rejected reloads retain the in-force policy. Existing bindings are re-evaluated on their next binding event.
Adds opt-in artifact signing to node-agent behind experimental_signing, while preserving default-build behavior and excluding signing dependencies from normal builds.
Changes:
Introduces signature verification, bundle assembly, trust-policy handling, and signed configuration support.
Integrates profile and rule admission with cache reconciliation and /policyz status reporting.
Adds signing fixtures, regression tests, build-tag support, CI checks, and documentation.
When bundleTrustPolicyPath is absent from the JSON file, setting only BUNDLETRUSTPOLICYPATH does not populate the field: AutomaticEnv does not register keys for viper.Unmarshal, and this key has neither a default nor an explicit binding. Consequently, configureSigning sees an empty path and never starts trust-policy loading or reload. Register the key alongside the verification flag and add an environment-only configuration test.
UserCPRef.Name can be <label>-<containerName> because initial population records the resolved per-container profile name. If a signed bundle named <label> becomes available later, including after policy reload, refresh searches for <label>-<containerName> instead and continues using the standalone profile. Newly started containers resolve the original pod label to the bundle, so enforcement differs by container age. Use the original pod label for bundle lookup and retain UserCPRef for the standalone-profile fallback.
Delay unsigned profile alert until section resolution
This emits R1018 before either caller runs resolveAuthoredSection. An unsigned grouped document with no section for this container, or a label-referenced learned profile, is subsequently discarded by that resolver but has already produced an “Unsigned profile governs” alert and consumed its deduplication key. Notify only after successful section resolution and adoption, in both initial population and refresh.
When initial population resolves a per-container profile (for example, pod label bundle37 resolves to bundle37-app), UserCPRef.Name stores the resolved profile name. Refresh therefore searches for bundle bundle37-app, while initial population searches for bundle37. Installing a valid bundle37 bundle later leaves existing containers on the flat profile, including after a policy reload. Preserve the original pod label separately for bundle discovery, keep UserCPRef for flat-profile fetches, and add a per-container flat-to-bundle transition test.
Report unsigned profiles only after successful adoption
In non-enforcing mode, this emits R1018 before the caller decides whether the profile can govern the container. An unsigned grouped document without a matching container section is reported here, then discarded by resolveAuthoredSection; a document carrying a learning-status annotation is also reported and discarded. These produce an "Unsigned profile governs" alert for content that was never adopted. Carry the unsigned verdict to the population and refresh callers, and notify only after resolution and adoption succeed. Add tests for both rejected-profile cases.
Gate cached content retention on signing eligibility, retain original bundle labels and flat retry references, and emit unsigned-governs alerts only after adoption.
Signed-off-by: Matthias Bertschy <matthias.bertschy@gmail.com>
Refusal rebuilds the entry with empty RV, UserCPRV, and checksum values, but records the current projection-spec hash. If the learned source is absent and the authored profile has an empty ResourceVersion, successfully re-signing that profile still satisfies every fast-skip condition in refreshOneEntry (lines 571–577). Verification succeeds and the entry is marked verified, but its empty projection is never replaced. Remote profiles may legitimately have empty ResourceVersions. Track refusal explicitly and bypass the unchanged-source fast path until admitted content has been projected; add a recovery regression with an absent learned source and an empty authored ResourceVersion.
Log unchanged bundle verification successes at Debug
pkg/signature/verify.go:104
This emits an Info record on every successful verification, including unchanged bundle fragments. The reconciler reassembles bundles for each cached container, so 100 containers using a 64-fragment bundle can produce 6,400 success records per refresh, even when nothing changes. This adds sustained log serialization and ingestion costs despite the bundle layer already logging root changes at Info. Log individual verification successes at Debug instead.
Refused entries bypass unchanged-source shortcuts until admitted content is projected. A regression reproduces absent learned data plus an authored profile with empty ResourceVersion, then checks that re-signing restores execution grants, completed state, and stable subsequent refreshes.
Individual verification successes now log at Debug; bundle root transitions remain visible at Info. The existing logging test was updated to preserve diagnostic-field coverage.
Affected tests, race tests, vet, default checks, and the experimental agent build pass locally.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Unresolved verification and policy-publication issues, combined with unvalidated end-to-end enforcement behavior, require fixes and final human review.
Prevent stale policy sync from overwriting reloaded rules
pkg/rulemanager/ruleswatcher/admission.go:67
The policy passed to admission can become stale before the result is published. In watcher_signing.go, trustPolicySnapshot releases policyMutex before admission and SyncRules. Meanwhile, the reload goroutine calls SetTrustPolicy and ResyncNow. A watch-triggered sync using the old policy can finish after the reload's resync and overwrite the new ruleset, restoring rules the new policy rejects—or unsigned rules if signing was previously disabled. These remain active until another sync. Coordinate policy replacement with admission and publication, or atomically validate a policy generation at publication and retry stale results. Add a regression that pauses an old sync and resumes it after reload completes.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive trust and admission changes need final human validation across both build variants, with unresolved correctness issues still requiring fixes.
Register bundle trust policy path for environment-only configuration
pkg/config/config_signing.go:35
Register bundleTrustPolicyPath with Viper, for example with an empty-string default. AutomaticEnv() does not include otherwise unknown environment-only keys in Unmarshal, so setting BUNDLETRUSTPOLICYPATH without that key in the configuration file leaves cfg.BundleTrustPolicyPath empty. configureSigning then skips policy loading and the reload watcher, leaving policy-driven profile, rule, and binding signing disabled. Add a regression that loads the policy path from the environment alone.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Cross-cutting trust changes, unresolved validation and tamper-reporting issues, and outstanding cluster validation require final human security review.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive trust and admission changes need human review, with unresolved correctness issues and Docker and cluster validation still outstanding.
With embedding enabled, SignObject can report success for content its verifier always rejects: decodeEmbeddedContent caps decompressed content at 8 MiB, but this path and the encoder impose no matching limit. Compression can make an oversized payload fit in a small annotation, so compressed size does not prevent this mismatch. Reject marshaled content above maxEmbeddedContentBytes here, before signing or modifying annotations. Add boundary tests for exactly the limit and one byte above it.
The reason will be displayed to describe this comment to others. Learn more.
🔵 Needs a closer look
Security-sensitive trust, admission, and concurrent enforcement changes need human sign-off, with Docker and cluster integration validation still outstanding.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Adds opt-in artifact signing behind the
experimental_signingGo build tag. Normal builds retain the baseline implementations and exclude the new signing packages. Build withmake binary BUILD_TAGS=experimental_signingor pass the same build argument to the Docker build.The experimental build supports signed ContainerProfile bundles, Rules and rule bindings; root-signed trust policies with guarded reload; signed config and clusterData;
/policyz; and thesign-objectCLI. Profile verification runs before grouped-section flattening, restores verified embedded content, rejects malformed embedded specs in either mode, and retries refused profiles after re-signing. Bundle assembly explicitly rejects grouped fragments; standalone grouped profiles remain supported. Binding selectors and rules come from authenticated signed content.This ports the signing feature from https://github.com/entlein/node-agent/pull/122 independently of https://github.com/entlein/node-agent/pull/121. It does not add RogueArtifact persistence, governance classification, or R1017 emission. R1016 handles tampering, R1018 reports unsigned governing profiles, and stored-spec divergence remains diagnostic.
Validation
Passed locally after rebasing onto upstream
c7a77c13(Go 1.27):Adds CI coverage for both variants and build-isolation checks. Docker images and cluster/component tests have not been run.
Integration and limitations
Rebased onto current upstream
c7a77c13. The branch contains the experimental signing implementation and its regression fixes; fork-only CI commits are excluded. The rebase preserves upstream host-sensor validation, dependency versions, canonical rule schemas, and watcher validation. It reuses upstream compact YAML and profile-list methods instead of adding duplicates. A regression test covers alert-to-enforce policy reload when upstream's checksum fetch optimization is active.Flat profiles retain the legacy verification behavior, which accepts self-signed certificates and detects content changes without bundle-class signer authorization. Signed bundles with a real trust root are required when signer authorization is needed. Invalid boot trust policies leave signing disabled until a valid policy is loaded; rejected reloads retain the in-force policy. Existing bindings are re-evaluated on their next binding event.