Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,15 @@ jobs:
# so the harness always builds the image. Letting that build be authoritative is what makes
# "push exactly what was tested" true BY CONSTRUCTION — there is only one build, so nothing can
# drift. Do NOT add a second `docker build` here, and do NOT add a skip-guard to
# integration-tests/pom.xml. APP_VERSION reaches the image label through the Compose service's
# build.args (see integration-tests/docker-compose.yml).
# integration-tests/pom.xml. APP_VERSION and APP_REVISION reach the image labels through the
# Compose service's build.args (see integration-tests/docker-compose.yml) — that is the only
# channel, because this lane performs no `docker build` of its own. APP_REVISION carries the
# release-tag commit resolved above, so org.opencontainers.image.revision names the very commit
# the `sha-` tag names.
- name: Run the integration-test suite (builds the image)
env:
APP_VERSION: ${{ steps.config.outputs.current-version }}
APP_REVISION: ${{ steps.commit.outputs.sha }}
run: |
./mvnw --no-transfer-progress verify -Pintegration-tests -pl integration-tests -am

Expand Down Expand Up @@ -374,6 +378,12 @@ jobs:
exit 1
fi

PULLED_REVISION="$(docker image inspect --format '{{index .Config.Labels "org.opencontainers.image.revision"}}' "$REF")"
if [ "$PULLED_REVISION" != "$TAG_SHA" ]; then
echo "::error::Published image label org.opencontainers.image.revision='${PULLED_REVISION}' but the release-tag commit is '${TAG_SHA}'."
exit 1
fi

CERTS="${GITHUB_WORKSPACE}/integration-tests/src/main/docker/certificates"
CONFIG="${GITHUB_WORKSPACE}/integration-tests/src/main/docker/sheriff-config"
docker run -d --name api-sheriff-smoke -p 19000:9000 \
Expand Down
9 changes: 6 additions & 3 deletions api-sheriff/src/main/docker/Dockerfile.native
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@
# quarkus.management.tls-configuration-name=plain-management.
FROM quay.io/quarkus/quarkus-distroless-image:2.0@sha256:58c30907e6d4417251c90963e1d14e6918a0baa6f8a7d046497584972dc2a378

# Supplied by the build (docker compose build.args.APP_VERSION). The `dev` default is deliberate and
# honest: a locally or PR-built image is never published, and a version-shaped default would lie the
# moment the project version moved. The release lane exports the released version into this arg.
# Both supplied by the build (docker compose build.args.APP_VERSION / build.args.APP_REVISION). The
# `dev` defaults are deliberate and honest: a locally or PR-built image is never published, and a
# version- or sha-shaped default would lie the moment the project version or the commit moved. The
# release lane exports the released version and the release-tag commit sha into these args.
ARG APP_VERSION=dev
ARG APP_REVISION=dev

LABEL org.opencontainers.image.title="API Sheriff"
LABEL org.opencontainers.image.description="Security-focused API Gateway — Quarkus native executable"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.revision="${APP_REVISION}"
LABEL org.opencontainers.image.vendor="CUIoss"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.source="https://github.com/cuioss/API-Sheriff"
Expand Down
8 changes: 7 additions & 1 deletion api-sheriff/src/main/docker/Dockerfile.native.jfr
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@
# No baked-in certificates — mount TLS certs at runtime via volumes or secrets
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0@sha256:a26d1276dee47e331805a2d89a0faa16dbfcf4ade3f7669a4dae16a752861677

# Supplied by the build (docker compose build.args.APP_VERSION); the jfr Compose overlay re-specifies
# only context and dockerfile, so it inherits that arg from the base service. The `dev` default is
# deliberate and honest: this profiling image is never published, and a version-shaped default would
# start lying the moment the project version moved.
ARG APP_VERSION=dev

LABEL org.opencontainers.image.title="API Sheriff - JFR"
LABEL org.opencontainers.image.description="Security-focused API Gateway — Quarkus native executable with JFR profiling"
LABEL org.opencontainers.image.version="0.1.0-SNAPSHOT"
LABEL org.opencontainers.image.version="${APP_VERSION}"
LABEL org.opencontainers.image.vendor="CUIoss"
LABEL org.opencontainers.image.licenses="Apache-2.0"
LABEL org.opencontainers.image.source="https://github.com/cuioss/API-Sheriff"
Expand Down
28 changes: 28 additions & 0 deletions doc/user/container-image.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ for you but works for a maintainer, this is the reason -- the field to check is
`visibility`, and only the literal value `public` serves anonymous consumers.
====

=== Reading the source commit off the image

The published image carries the same commit in an OCI label, so you can recover it from an image you
already hold without knowing which tag it was pulled under:

[source,bash]
----
docker image inspect \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}' \
ghcr.io/cuioss/api-sheriff@sha256:...
----

The value is the full 40-character SHA of the release-tag commit -- the same commit
`ghcr.io/cuioss/api-sheriff:sha-<commit>` names. `org.opencontainers.image.version` sits beside it
and carries the released version. The release lane reads both off the *pulled* image and fails the
release when either disagrees with what it just published, so a released image whose labels lie does
not ship.

*This is a convenience, not a security control.* An image label is ordinary image metadata: anyone
who can build an image can write any value into it, and reading one back proves nothing about where
the image came from. Provenance authority remains the Cosign certificate -- verify it as described
under <<_verify_the_signature_before_deploying,Verify the signature before deploying>> and treat the
label as a lookup aid once that verification has passed.

A locally or PR-built image carries `dev` in both labels. That default is deliberate: such an image
is never published, and a version- or SHA-shaped placeholder would read as a claim about a release
that does not exist.

== Pull and run

The image is distroless: it carries no shell and no package manager, and it runs as `nonroot`. It
Expand Down
8 changes: 6 additions & 2 deletions integration-tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,10 +170,14 @@ services:
dockerfile: src/main/docker/Dockerfile.native
# The harness build is the ONE image build in this project — the release lane runs this very
# compose build and pushes exactly the digest it produced — so this is the single supply point
# for the image's org.opencontainers.image.version label. The release lane exports
# APP_VERSION=<released version> into the Maven step's environment; locally it stays `dev`.
# for the image's org.opencontainers.image.version and .revision labels. release.yml performs
# no docker build of its own, which makes these build.args the ONLY channel reaching the
# Dockerfile ARGs. The release lane exports APP_VERSION=<released version> and
# APP_REVISION=<release-tag commit sha> into the Maven step's environment; locally both stay
# `dev`.
args:
APP_VERSION: ${APP_VERSION:-dev}
APP_REVISION: ${APP_REVISION:-dev}
cache_from:
- quay.io/quarkus/quarkus-distroless-image:2.0
- quay.io/quarkus/ubi9-quarkus-micro-image:2.0
Expand Down
35 changes: 35 additions & 0 deletions integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,14 @@
<includes>
<include>**/integration/**/*IT.java</include>
</includes>
<!-- The mirror image of the jfr profile's exclusion below:
ImageMetadataJfrIT asserts the version label on
api-sheriff:jfr, which only the jfr lane builds. This lane
builds and tags api-sheriff:distroless, so the IT would
inspect an image this profile never produced. -->
<excludes>
<exclude>**/ImageMetadataJfrIT.java</exclude>
</excludes>
<systemPropertyVariables>
<test.https.port>${test.https.port}</test.https.port>
<!-- mTLS handshake suite: the dedicated api-sheriff-mtls instance
Expand Down Expand Up @@ -420,6 +428,15 @@
<includes>
<include>**/integration/**/*IT.java</include>
</includes>
<!-- ImageMetadataIT asserts the OCI labels on api-sheriff:distroless,
but this lane builds and tags api-sheriff:jfr — the IT would
inspect an image this profile never produced. The JFR image's
own version label IS machine-asserted, by ImageMetadataJfrIT,
which runs in this lane against the image the docker-build-jfr
execution below builds. -->
<excludes>
<exclude>**/ImageMetadataIT.java</exclude>
</excludes>
Comment thread
cuioss-oliver marked this conversation as resolved.
<systemPropertyVariables>
<test.https.port>${test.https.port}</test.https.port>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
Expand Down Expand Up @@ -447,6 +464,24 @@
<workingDirectory>${project.basedir}</workingDirectory>
</configuration>
</execution>
<!-- Prepare the JFR bind-mount host directory BEFORE any compose command
resolves the jfr overlay: whichever compose command touches it first
would otherwise create it root-owned, and the uid-1001 container could
not write the recording. See scripts/prepare-jfr-output-dir.sh. -->
<execution>
<id>prepare-jfr-output-dir</id>
<phase>pre-integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>./scripts/prepare-jfr-output-dir.sh</executable>
<workingDirectory>${project.basedir}</workingDirectory>
<arguments>
<argument>${project.basedir}</argument>
</arguments>
</configuration>
</execution>
<!-- Build Docker image with native JFR support -->
<execution>
<id>docker-build-jfr</id>
Expand Down
26 changes: 26 additions & 0 deletions integration-tests/scripts/prepare-jfr-output-dir.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# Prepare the host directory that docker-compose.jfr.yml bind-mounts at /tmp/jfr-output.
#
# The JFR image runs as uid 1001, and the Dockerfile's own `chmod 777 /tmp/jfr-output` does NOT
# survive the mount — a bind mount shadows the image's directory with the host's, ownership
# included. Whichever `docker compose` command touches the service first creates the missing host
# directory as root:root 0755, after which uid 1001 cannot write and the gateway dies at startup
# with "Could not start recording, not able to write to file /tmp/jfr-output/api-sheriff-profile.jfr".
# That is a startup failure, not a degraded profile: the JFR lane never comes up at all.
#
# So this must run before ANY compose command that resolves the jfr overlay. There are two such
# entry points and each is genuinely the first toucher on its own path, which is why both call
# this one script rather than open-coding it:
# - the `jfr` Maven profile, whose `docker-build-jfr` execution composes the overlay to build;
# - start-integration-container.sh, for a lane that composes the overlay without that execution
# having run (the benchmark lane reuses an already-present api-sheriff:jfr image).
#
# Mode 1777, not 0777, for the same reason the /logs mount uses it: the sticky bit keeps the world
# write from also being a world DELETE of the recording this lane exists to produce.
set -euo pipefail

JFR_TARGET_DIR="${1:?usage: prepare-jfr-output-dir.sh <integration-tests-dir>}/target/jfr-recordings"

mkdir -p "${JFR_TARGET_DIR}"
chmod 1777 "${JFR_TARGET_DIR}"
echo "📁 JFR recordings will be written to: ${JFR_TARGET_DIR}/api-sheriff-profile.jfr"
8 changes: 8 additions & 0 deletions integration-tests/scripts/start-integration-container.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@ mkdir -p "${LOG_TARGET_DIR}"
chmod 1777 "${LOG_TARGET_DIR}"
echo "📁 Quarkus logs will be written to: ${LOG_TARGET_DIR}/quarkus.log"

# The JFR overlay bind-mounts ./target/jfr-recordings at /tmp/jfr-output, and that host directory
# must exist and be container-writable before the first compose command resolves the overlay —
# see scripts/prepare-jfr-output-dir.sh for why, and for why both this script and the `jfr` Maven
# profile call it. Only the JFR overlay needs it, so it runs only when that overlay is composed.
if [[ "$IMAGE_TYPE" == "jfr" ]]; then
./scripts/prepare-jfr-output-dir.sh "${PROJECT_DIR}"
fi

# Discover every host-side probe target from the resolved Compose model, BEFORE anything is started.
#
# The service set, each service's published management port, and the scheme its management interface
Expand Down
Loading
Loading