fix: validate release tags by Gitea run ID
This commit is contained in:
1 parent
8f331a002f
commit
779743fdb0
6 files changed
+120
-21
No files matched your search
@@ -1,3 +1,4 @@
|
||||
FROM ghcr.io/osscontainertools/kaniko:v1.28.5-debug@sha256:c73ca67a3b494bce79a8fb6090c4f73c18997ab272d80bffc460a62343aeb412
|
||||
COPY release-build.sh /usr/local/bin/release-build
|
||||
COPY validate-destination.sh /usr/local/bin/validate-destination.sh
|
||||
ENTRYPOINT ["/busybox/sh", "/usr/local/bin/release-build"]
|
||||
@@ -7,20 +7,7 @@ destination=$3
|
||||
digest_file=$4
|
||||
: "${REGISTRY_USERNAME:?registry username secret is required}"
|
||||
: "${REGISTRY_PASSWORD:?registry password secret is required}"
|
||||
|
||||
case "$dockerfile" in
|
||||
deploy/production/Dockerfile.backend)
|
||||
printf '%s\n' "$destination" | grep -Eq '^git\.vakanaut\.ru/admin/akyldash/backend:[0-9a-f]{40}-[0-9]+-[1-9][0-9]*$' || {
|
||||
echo 'ERROR: backend destination must be a commit/run/attempt tag in the approved registry namespace' >&2; exit 2;
|
||||
}
|
||||
;;
|
||||
deploy/production/Dockerfile.opensearch)
|
||||
printf '%s\n' "$destination" | grep -Eq '^git\.vakanaut\.ru/admin/akyldash/opensearch:[0-9a-f]{40}-[0-9]+-[1-9][0-9]*$' || {
|
||||
echo 'ERROR: OpenSearch destination must be a commit/run/attempt tag in the approved registry namespace' >&2; exit 2;
|
||||
}
|
||||
;;
|
||||
*) echo 'ERROR: Dockerfile is outside the approved production image set' >&2; exit 2 ;;
|
||||
esac
|
||||
/busybox/sh /usr/local/bin/validate-destination.sh "$dockerfile" "$destination"
|
||||
|
||||
mkdir -p /kaniko/.docker "$(dirname "$digest_file")"
|
||||
auth=$(printf '%s:%s' "$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 2 ]; then
|
||||
echo 'ERROR: expected Dockerfile path and image destination' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
dockerfile=$1
|
||||
destination=$2
|
||||
case "$dockerfile" in
|
||||
deploy/production/Dockerfile.backend)
|
||||
repository=git.vakanaut.ru/admin/akyldash/backend
|
||||
;;
|
||||
deploy/production/Dockerfile.opensearch)
|
||||
repository=git.vakanaut.ru/admin/akyldash/opensearch
|
||||
;;
|
||||
*)
|
||||
echo 'ERROR: Dockerfile is outside the approved production image set' >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$destination" in
|
||||
"$repository":*) tag=${destination#"$repository":} ;;
|
||||
*)
|
||||
echo 'ERROR: destination is outside the approved registry repository' >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
commit=${tag%%-*}
|
||||
run_id=${tag#*-}
|
||||
case "$commit" in
|
||||
''|*[!0123456789abcdef]*)
|
||||
echo 'ERROR: destination commit must be a lowercase Git SHA-1' >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
if [ "${#commit}" -ne 40 ] || [ "$run_id" = "$tag" ]; then
|
||||
echo 'ERROR: destination tag must contain a 40-character commit and run ID' >&2
|
||||
exit 2
|
||||
fi
|
||||
case "$run_id" in
|
||||
''|*[!0123456789]*)
|
||||
echo 'ERROR: destination run ID must be numeric' >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -122,7 +122,7 @@ Gitea account/token with write access to the two repository packages and no
|
||||
unneeded administration rights. Do not reuse a runner or production host
|
||||
credential. The workflow uses the Gitea host and fixed package namespace
|
||||
`git.vakanaut.ru/admin/akyldash/{backend,opensearch}`, pushes unique
|
||||
commit/run/attempt tags, captures registry digests, validates the release
|
||||
commit/run-ID tags, captures registry digests, validates the release
|
||||
descriptor, and uploads the JSON plus SHA-256 sidecar as a Gitea Actions
|
||||
artifact using a full-SHA-pinned Gitea-compatible v3 action. PR CI also uploads
|
||||
a tiny synthetic marker with one-day retention to verify the runner's runtime
|
||||
@@ -132,7 +132,7 @@ artifact compatibility is based on the runtime API supported by act_runner
|
||||
artifact retention must preserve release digests and descriptors for the full
|
||||
rollback window; the owner must confirm both policies.
|
||||
|
||||
The descriptor records source repository/ref/commit, workflow run and attempt,
|
||||
The schema-v3 descriptor records source repository/ref/commit, workflow run ID,
|
||||
pinned Kaniko builder and both image digests. It is an unsigned trace record,
|
||||
not a cryptographic attestation; deployment relies on retrieving it from the
|
||||
authenticated Gitea run artifact and verifying its checksum and repository
|
||||
|
||||
@@ -158,11 +158,10 @@ Docker state, UFW rule details, and protected file metadata were not verified.
|
||||
- The manual image release workflow is restricted to workflow dispatch from
|
||||
protected `main`; PR image builds remain no-push. It requires owner-configured
|
||||
`REGISTRY_USERNAME` and `REGISTRY_PASSWORD`, validates published digests
|
||||
against an explicit repository allowlist, and prints a commit/run/attempt
|
||||
descriptor in the authenticated run log (runner artifact/cache support stays
|
||||
disabled). The descriptor is unsigned trace metadata, not cryptographic
|
||||
attestation. Registry write rights and digest retention are not configured
|
||||
or verified.
|
||||
against an explicit repository allowlist, and uploads a schema-v3
|
||||
commit/run-ID descriptor with SHA-256 as an Actions artifact. The descriptor
|
||||
is unsigned trace metadata, not cryptographic attestation. Registry write
|
||||
rights and digest retention are not configured or verified.
|
||||
- A repository-scoped official `act_runner` `0.2.13` installation is
|
||||
configured in `infra/runner/`: rootless Docker under `akyldash-runner`, 15 GiB
|
||||
ext4 loop-backed image/workspace storage, capacity 1, per-job limits (2 CPU,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import os
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
@@ -44,6 +46,67 @@ class ReleaseWorkflowTests(unittest.TestCase):
|
||||
self.assertNotIn("DOCKER_HOST", push_action)
|
||||
self.assertIn("--digest-file=", push_action)
|
||||
|
||||
def test_release_validator_accepts_workflow_tags_without_credentials_or_kaniko(self):
|
||||
workflow = (ROOT / ".gitea/workflows/release-images.yaml").read_text(encoding="utf-8")
|
||||
dockerfile = ROOT / ".gitea/actions/kaniko-push/Dockerfile"
|
||||
release_builder = (ROOT / ".gitea/actions/kaniko-push/release-build.sh").read_text(encoding="utf-8")
|
||||
validator = ROOT / ".gitea/actions/kaniko-push/validate-destination.sh"
|
||||
self.assertIn(
|
||||
"COPY validate-destination.sh /usr/local/bin/validate-destination.sh",
|
||||
dockerfile.read_text(encoding="utf-8"),
|
||||
)
|
||||
self.assertIn(
|
||||
'/busybox/sh /usr/local/bin/validate-destination.sh "$dockerfile" "$destination"',
|
||||
release_builder,
|
||||
)
|
||||
self.assertIn(
|
||||
"destination: git.vakanaut.ru/admin/akyldash/backend:${{ github.sha }}-${{ github.run_id }}",
|
||||
workflow,
|
||||
)
|
||||
self.assertIn(
|
||||
"destination: git.vakanaut.ru/admin/akyldash/opensearch:${{ github.sha }}-${{ github.run_id }}",
|
||||
workflow,
|
||||
)
|
||||
examples = (
|
||||
("deploy/production/Dockerfile.backend", "git.vakanaut.ru/admin/akyldash/backend"),
|
||||
("deploy/production/Dockerfile.opensearch", "git.vakanaut.ru/admin/akyldash/opensearch"),
|
||||
)
|
||||
environment = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")}
|
||||
for dockerfile_path, repository in examples:
|
||||
destination = f"{repository}:{'a' * 40}-567"
|
||||
with self.subTest(repository=repository):
|
||||
accepted = subprocess.run(
|
||||
["sh", str(validator), dockerfile_path, destination],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
)
|
||||
self.assertEqual(accepted.returncode, 0, accepted.stderr)
|
||||
|
||||
for invalid in (
|
||||
destination + "-1",
|
||||
destination.replace(repository, "registry.invalid/other/image"),
|
||||
f"{repository}:{'A' * 40}-567",
|
||||
f"{repository}:{'a' * 39}-567",
|
||||
f"{repository}:{'a' * 40}-",
|
||||
f"{repository}:{'a' * 40}-not-a-run-id",
|
||||
):
|
||||
rejected = subprocess.run(
|
||||
["sh", str(validator), dockerfile_path, invalid],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
)
|
||||
self.assertNotEqual(rejected.returncode, 0, invalid)
|
||||
|
||||
rejected_dockerfile = subprocess.run(
|
||||
["sh", str(validator), "deploy/production/Dockerfile.unapproved", destination],
|
||||
text=True,
|
||||
capture_output=True,
|
||||
env=environment,
|
||||
)
|
||||
self.assertNotEqual(rejected_dockerfile.returncode, 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user