STAC-25457 Publish and sign the agent and cluster-agent images from GitHub Actions - #454
Conversation
…roxy BCI bases Address review feedback on #454. Gate publication on the image scan: build-agent-image and build-cluster-agent-image now run image-pipeline's scan-image in `mode: gate` at the house severity (UNKNOWN..CRITICAL, Grype enabled) directly after the smoke test. The publish jobs already depend on the build jobs, so a failing scan now blocks publication instead of only informing. Because build-agent-image also runs for same-repo pull requests, the gate executes on this PR's own CI. Make publication depend on all verification jobs: publish-agent-image now needs test-deb-renaming as well as build-agent-image, so a .deb that still carries DataDog branding cannot reach quay.io. test-deb-renaming lives in this workflow only, so the cluster-agent lane is unaffected. Pull the BCI bases through the SUSE proxy: both Dockerfiles take BCI_IMAGE_REGISTRY as a build argument, defaulting to registry.tooling.stackstate.io/suse/bci. This follows the existing BASE_IMAGE_REGISTRY convention in the same files, so every build path picks it up -- the GitHub workflows, the GitLab jobs, and build_images.sh -- without each caller having to pass a build argument. The apply-oci-labels base-name input stays on registry.suse.com so the provenance label keeps the upstream identity that VEX data is keyed on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…roxy BCI bases Address review feedback on #454. Gate publication on the image scan: build-agent-image and build-cluster-agent-image now run image-pipeline's scan-image in `mode: gate` at the house severity (UNKNOWN..CRITICAL, Grype enabled) directly after the smoke test. The publish jobs already depend on the build jobs, so a failing scan now blocks publication instead of only informing. Because build-agent-image also runs for same-repo pull requests, the gate executes on this PR's own CI. Make publication depend on all verification jobs: publish-agent-image now needs test-deb-renaming as well as build-agent-image, so a .deb that still carries DataDog branding cannot reach quay.io. test-deb-renaming lives in this workflow only, so the cluster-agent lane is unaffected. Pull the BCI bases through the SUSE proxy: both Dockerfiles take BCI_IMAGE_REGISTRY as a build argument, defaulting to registry.tooling.stackstate.io/suse/bci. This follows the existing BASE_IMAGE_REGISTRY convention in the same files, so every build path picks it up -- the GitHub workflows, the GitLab jobs, and build_images.sh -- without each caller having to pass a build argument. The apply-oci-labels base-name input stays on registry.suse.com so the provenance label keeps the upstream identity that VEX data is keyed on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2d5cbce to
61e2bce
Compare
Scan gate results, and what the gate foundThe gate is now wired on both images with Secret scanning is clean on both images. Vulnerability scanning found a real backlog, which I have declared explicitly rather than hidden by weakening the severity filter:
Every finding has a file under The runtime bumps are omnibus software-definition changes rather than anything a workflow in this repo can do, so they are tracked separately in STAC-25556.
Known gap: arm64 image scansThe arm64 scan jobs fail with "VEX repositories downloaded, but no OpenVEX documents were found for Grype". That message is misleading. The real cause is that Evidence: arm64 skipped all 1105 documents in 830 ms (~0.75 ms each, too fast for a process spawn) while amd64 skipped only 3 at ~32 ms each; and extracting both runner images shows The fix belongs in the arm64 runner image ( YAML commentsPer house convention I have removed the whole-line comments from the workflow YAML across this stack. Shell comments inside |
Ports the publishing half of pre_release_main_agent_image and pre_release_cluster_agent_image, plus merge_docker_manifest_main_agent and merge_docker_manifest_cluster_agent, from GitLab to GitHub Actions. PR #446 landed the build-and-verify halves; this adds the push. Each image gets a per-arch publish job that needs: the existing image build job, and a manifest-merge job that assembles the multi-arch tag from the two single-arch ones. Gated on `github.event_name == 'push'` alone: the workflow's push filter only carries the release branch, so the event check is the whole gate, and a workflow_dispatch on an arbitrary branch cannot publish. This is the same reasoning the cerberus-notify gate already uses. Uses the StackVista/image-pipeline composite actions rather than an open-coded docker push, matching stackstate-process-agent and the STAC-24837 direction for product repos. Over publish_image.sh that adds cosign signatures in both v2 and v3 bundle formats, SBOM and max-mode provenance attestations, canonical SUSE Observability OCI labels, an entrypoint ELF-architecture check that catches arch-mismatched images before they are signed, and refusal to overwrite an existing tag. Neither Dockerfile declares `ARG BASE_IMAGE`, so base-name is passed explicitly; both final stages are registry.suse.com/bci/bci-micro. Tag is the 8-character short SHA, matching GitLab's CI_COMMIT_SHORT_SHA. The `<branch>-<arch>` tag publish_image.sh also pushed is deliberately dropped: helm-charts-internal pins the agent and cluster-agent images to an 8-character SHA (currently 9516cb4, the stackstate-7.78.2 HEAD), and beest receives the tag as AGENT_HASH_UNDER_TEST, so nothing consumes a branch-name tag. An org-wide code search for stackstate-k8s-agent:master, :stackstate-7*, :$CI_COMMIT_REF_SLUG and the cluster-agent equivalents returns no hits, verified against a positive control so an empty result is not a false negative. Both publish jobs need id-token: write for keyless cosign signing, and are added to each workflow's cerberus-notify needs list so a failed publish on the release branch still reaches Slack. Requires STAC-25541 (terraform-infra #75): the stackstate+agent robot behind QUAY_USER has no write grant on stackstate-k8s-agent or stackstate-k8s-cluster-agent, since GitLab publishes them with the legacy gitlabci robot instead. Without it these jobs fail exactly as process-agent did in STAC-25510. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…roxy BCI bases Address review feedback on #454. Gate publication on the image scan: build-agent-image and build-cluster-agent-image now run image-pipeline's scan-image in `mode: gate` at the house severity (UNKNOWN..CRITICAL, Grype enabled) directly after the smoke test. The publish jobs already depend on the build jobs, so a failing scan now blocks publication instead of only informing. Because build-agent-image also runs for same-repo pull requests, the gate executes on this PR's own CI. Make publication depend on all verification jobs: publish-agent-image now needs test-deb-renaming as well as build-agent-image, so a .deb that still carries DataDog branding cannot reach quay.io. test-deb-renaming lives in this workflow only, so the cluster-agent lane is unaffected. Pull the BCI bases through the SUSE proxy: both Dockerfiles take BCI_IMAGE_REGISTRY as a build argument, defaulting to registry.tooling.stackstate.io/suse/bci. This follows the existing BASE_IMAGE_REGISTRY convention in the same files, so every build path picks it up -- the GitHub workflows, the GitLab jobs, and build_images.sh -- without each caller having to pass a build argument. The apply-oci-labels base-name input stays on registry.suse.com so the provenance label keeps the upstream identity that VEX data is keyed on. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tions The image scan gate added in this branch surfaced a real backlog: 24 findings in the agent image (embedded CPython 3.13.13, cryptography 48.0.1, two Go advisories) and 2 in the cluster-agent image. Secret scanning is clean on both. Keep the gate at mode: gate and declare every finding explicitly instead of weakening the severity filter, so nothing new can slip in unnoticed. Each exception carries a <=14-day expiry per the CVE remediation SLA, so the gate starts failing again if the underlying bumps stall. The omnibus runtime bumps (CPython 3.13.14, cryptography) are tracked in STAC-25556; they are software-definition changes, not workflow changes. LOCAL_IMAGE is renamed to the quay path the publish jobs already use because the evaluator keys exceptions on the normalised image name, which strips only the tag or digest and not the registry or namespace. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…itHub Closes the last two GitLab jobs with no GitHub counterpart: sign_deb and pre_release_deb (.gitlab-ci-agent.yml lines 604 and 635). Security prerequisite, not cleanup ---------------------------------- sign_debian_package.sh called printenv unconditionally. On GitLab that dumped the GPG private key and its passphrase into the job log; stackstate-agent is a PUBLIC repo, so on GitHub that log is world-readable. It also left the exported private key in the checkout as gpg_private.key, where any later artifact upload would collect it. Both are removed before any signing secret is wired in. Key setup now happens in an ephemeral GNUPGHOME created with mktemp and mode 700, removed by an EXIT trap that also kills the gpg-agent so a preset passphrase cannot outlive the job on a reused runner. The passphrase reaches gpg through a mode-600 file inside that directory rather than argv. Fixes a latent signing bug -------------------------- The old preset step interpolated an unquoted command substitution into a single gpg-preset-passphrase call. A key exposes one keygrip per primary and subkey, so with a signing subkey the second keygrip was passed as a stray argument and never presetted, leaving signing able to block on a pinentry prompt no CI runner can answer. Each keygrip is now presetted individually. Verified against a throwaway key: two keygrips, both presetted. Shared setup ------------ sign_debian_package.sh and publish_package.sh need the same key in the same state but are separate processes, and on GitHub may be separate steps, so neither can rely on a keyring the other left behind. The setup moves into gpg_signing_setup.sh, sourced by both. gpg-preset-passphrase is auto-detected across /usr/lib/gnupg2, /usr/lib/gnupg, /usr/libexec and PATH, and the script fails loudly rather than silently skipping the preset when it is absent. Both scripts now fall back from CI_PROJECT_DIR to GITHUB_WORKSPACE, and publish_package.sh falls back from CI_COMMIT_REF_NAME to GITHUB_REF_NAME, so the apt codename stays the release branch exactly as it was on GitLab. Workflow -------- sign-and-publish-deb downloads both architecture artifacts and runs a single deb-s3 upload. GitLab fanned this out per architecture, so two jobs rewrote the same apt index concurrently; collecting both first removes that race. The install script is split in two. generate-install-script runs inv release.generate-install -t inside the build container, because the task collection imports python-gitlab and the GitHub and Datadog API helpers and only loads in the conda environment, and asserts the rendered script contains no None.s3.amazonaws.com from an unset bucket variable. publish-install-script then uploads it, so the container job never holds a credential. Both publishing jobs are gated on push and bound to the agent-pre-release environment, which carries the deployment branch rule, the signing secrets and the AWS role from STAC-25545. Pull requests cannot reach them. Validated: shellcheck -x clean on all three scripts; gpg_signing_setup exercised against a generated throwaway key covering explicit override, PATH auto-detection and the missing-binary failure path; actionlint clean apart from the pre-existing self-hosted runner-label notices; zizmor reports no findings. Blocked until the agent-pre-release environment, its four SIGNING_* secrets and AGENT_PRERELEASE_ROLE_ARN exist. Refs STAC-25546, STAC-25545 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tion Addresses review feedback on PR #455. [P1] The signing job installed the publisher with `gem install deb-s3`, resolving the latest code at run time into a job that then executes it with the package signing key and the pre-release AWS credentials in scope. GitLab never did this: `sign_deb` ran in a pinned image with deb-s3 already baked in, so the runtime resolve was a regression introduced by the port. deb-s3 and its full runtime dependency tree are now pinned in `.github/deb-s3-gems.sha256` and installed by `.github/scripts/install-deb-s3.sh`, which fetches each gem at its exact version and verifies it against the SHA256 RubyGems publishes for that release before anything is installed or executed. The manifest covers nine gems. base64, bigdecimal and logger are deliberately excluded: aws-sdk-core requires them at ">= 0" and they are Ruby default gems supplied by the distribution's own ruby package, so pinning them would force a native build for no supply-chain gain. The script also links the canonical executable when RubyGems installs a versioned binstub, since publish_package.sh invokes deb-s3 by bare name, and ends with a `deb-s3 help` smoke check that activates the whole pinned set so a missing or incompatible gem fails at install time rather than midway through publishing. [P2] publish-install-script depended only on generate-install-script, so it could overwrite the public install.sh even when the build, signing or apt upload had failed. GitLab's pre_release_deb required sign_deb. It now requires both generate-install-script and sign-and-publish-deb, restoring that release boundary. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Resolving the rebase conflict against the rewritten base took this branch's whole copy of build-deb.yml, which predated the exception work on STAC-25457. That silently reverted two lines, so the gate ran here with no exceptions loaded and reported all 24 findings as unmanaged. Restores the quay LOCAL_IMAGE name the evaluator matches exceptions on, and the exceptions-path input. The workflow diff against the base branch is now additions only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
d7e06f4 to
ca28691
Compare
61e2bce to
19fa3b9
Compare
STAC-25500 Port the DEB signing and pre-release publishing lane to GitHub Actions
Ports the publishing half of
pre_release_main_agent_image/pre_release_cluster_agent_imageand bothmerge_docker_manifest_*jobs from GitLab. #446 landed the build-and-verify halves; this adds the push.Stacked on
STAC-25142-agent-lint-unit(#444), like #446 and #448-#452.What lands
publish-agent-image(amd64, arm64)pre_release_main_agent_image(push half)merge-agent-manifestmerge_docker_manifest_main_agentpublish-cluster-agent-image(amd64, arm64)pre_release_cluster_agent_image(push half)merge-cluster-agent-manifestmerge_docker_manifest_cluster_agentEach publish job
needs:the existing image-build job; each merge job assembles the multi-arch tag from the two single-arch ones. All four are added to their workflow'scerberus-notifyneedslist, so a failed publish on the release branch still reaches Slack.Gating
if: github.event_name == 'push'. The workflow'spush:filter only carries the release branch, so the event check is the whole gate — and aworkflow_dispatchon an arbitrary branch cannot publish. Same reasoning the existingcerberus-notifygate uses. PRs build and smoke-test images but never push, which is the point on a public repo.image-pipeline instead of publish_image.sh
Uses the
StackVista/image-pipelinecomposite actions, matchingstackstate-process-agentand the STAC-24837 direction for product repos. Overpublish_image.shthat buys:exec format erroron a nodeNeither Dockerfile declares
ARG BASE_IMAGE, sobase-nameis passed explicitly — both final stages areregistry.suse.com/bci/bci-micro. The agent build also needs--build-arg ARCH; the cluster-agent takes none.Dropped: the
<branch>-<arch>tagpublish_image.shpushed both<branch>-<arch>and<short-sha>-<arch>. Only the short SHA is kept, because nothing consumes the branch tag:helm-charts-internalpins both images to an 8-character SHA (currently9516cb41—stackstate-7.78.2HEAD)AGENT_HASH_UNDER_TEST, which GitLab set fromCI_COMMIT_SHORT_SHAstackstate-k8s-agent:master,:stackstate-7*,:$CI_COMMIT_REF_SLUGand the cluster-agent equivalents returns 0 hits, run against a positive control so an empty result is not a false negativeTag is
cut -c1-8of the commit SHA, matching GitLab'sCI_COMMIT_SHORT_SHAand the chart's pin width.These jobs will fail until StackVista/terraform-infra#75 applies. The
stackstate+agentrobot behindQUAY_USERhas no write grant onstackstate-k8s-agentorstackstate-k8s-cluster-agent— neither repo appears in any team block inquay/locals.tf. GitLab publishes them with the separate legacygitlabcirobot, whose access predates Terraform, which is why this gap is invisible until the GitHub lane runs. It is the same failure that blocked process-agent in STAC-25510.Still out of scope
pre_release_deb(S3install.shpublish) andsign_deb(GPG key material) remain unported — both are release-lane concerns needing credentials that do not exist on this repo yet.Validation
actionlint: no new findings (only the pre-existing self-hostedrunner-labelnoise, which is unavoidable without anactionlint.yaml)zizmor:No findings to reportneeds:reference resolves against a real job in both workflowsJira: https://stackstate.atlassian.net/browse/STAC-25457