fix: validate release tags by Gitea run ID
CI / tests-and-images (pull_request) Successful in 2m18s
Runner smoke test / runner-smoke (pull_request) Successful in 1s

This commit is contained in:
agent committed 2026-10-26 10:56:59 +03:00
1 parent 8f331a002f
commit 779743fdb0
6 files changed
+120 -21

No files matched your search

+1
View File
@@ -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"]
+1 -14
View File
@@ -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
+2 -2
View File
@@ -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
+4 -5
View File
@@ -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,
+63
View File
@@ -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()