From 3e830391124b32a13b2e2c32eb31ee597606a12c Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 25 Aug 2026 14:46:00 -0300 Subject: [PATCH 1/2] Put metrics behind auth in `enterprise/e2e/public` (for Grafana) Signed-off-by: Juan Cruz Viotti --- .github/workflows/deploy.yml | 16 +++++ enterprise/e2e/public/compose.yml | 2 + enterprise/e2e/public/environment | 1 + enterprise/e2e/public/hurl/metrics.all.hurl | 71 +++++++++++++++++++++ enterprise/e2e/public/one.json | 18 ++++++ 5 files changed, 108 insertions(+) create mode 100644 enterprise/e2e/public/environment create mode 100644 enterprise/e2e/public/hurl/metrics.all.hurl diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d717a38eb..f59911fe0 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -282,15 +282,31 @@ jobs: run: | set -o errexit set -o nounset + # An unset secret reaches here as an empty string rather than as + # nothing, which `nounset` does not catch, so it is said outright + test -n "$SOURCEMETA_ONE_METRICS_TOKEN" + # The instance is given the digest of the metrics key rather than + # the key, so what it holds cannot be presented as a credential + METRICS_DIGEST="$(printf '%s' "$SOURCEMETA_ONE_METRICS_TOKEN" | sha256sum | cut -d' ' -f1)" kraft cloud service create --name schemas --domain ${{ env.SERVICE_DOMAIN }} 443:8000 || true kraft cloud deploy --memory 4096 --rollout remove --image schemas --service schemas \ + --env "SOURCEMETA_ONE_METRICS_KEY=$METRICS_DIGEST" \ --kraftfile enterprise/e2e/public/Kraftfile --restart on-failure --replicas 4 ./enterprise/e2e/public kraft cloud service get schemas kraft cloud service get schemas --output json kraft cloud instance logs "$(kraft cloud service get schemas --output json | jq --raw-output '.data.service_groups[0].instances[0].name')" curl --location --header "Accept: text/html" --retry 10 --retry-all-errors --fail https://${{ env.SERVICE_DOMAIN }} + # The gate is live, so an anonymous scrape is refused + curl --silent --output /dev/null --retry 10 --retry-all-errors \ + --write-out '%{http_code}' https://${{ env.SERVICE_DOMAIN }}/self/v1/metrics | grep --quiet '^401$' + # And the digest reached the instance intact, so the real scraper + # will be admitted rather than silently refused forever + curl --silent --fail --retry 10 --retry-all-errors \ + --header "Authorization: Bearer $SOURCEMETA_ONE_METRICS_TOKEN" \ + https://${{ env.SERVICE_DOMAIN }}/self/v1/metrics | grep --quiet '^# TYPE sourcemeta_one_build_info gauge$' env: KRAFTCLOUD_TOKEN: ${{ secrets.KRAFTCLOUD_TOKEN }} + SOURCEMETA_ONE_METRICS_TOKEN: ${{ secrets.SOURCEMETA_ONE_METRICS_TOKEN }} # Update deployment status - if: ${{ steps.deploy.outcome == 'success' }} diff --git a/enterprise/e2e/public/compose.yml b/enterprise/e2e/public/compose.yml index 74dfbbd9c..dfe88b17a 100644 --- a/enterprise/e2e/public/compose.yml +++ b/enterprise/e2e/public/compose.yml @@ -7,5 +7,7 @@ services: SOURCEMETA_ONE: one environment: - SOURCEMETA_ONE_PORT=8001 + env_file: + - environment ports: - "${PORT}:8001" diff --git a/enterprise/e2e/public/environment b/enterprise/e2e/public/environment new file mode 100644 index 000000000..a7be3f138 --- /dev/null +++ b/enterprise/e2e/public/environment @@ -0,0 +1 @@ +SOURCEMETA_ONE_METRICS_KEY=ed20191044553dac8f9c45e62062dd18e7dc1f898a897240b4179fb84fea3db4 diff --git a/enterprise/e2e/public/hurl/metrics.all.hurl b/enterprise/e2e/public/hurl/metrics.all.hurl new file mode 100644 index 000000000..c8df64349 --- /dev/null +++ b/enterprise/e2e/public/hurl/metrics.all.hurl @@ -0,0 +1,71 @@ +# A policy on /self/v1/metrics gates the scrape surface at dispatch: denied +# without a credential, with the canonical 401 shape +GET {{base}}/self/v1/metrics +HTTP 401 +Cache-Control: no-store +Content-Type: application/problem+json +WWW-Authenticate: Bearer realm="registry" +Link: ; rel="describedby" +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: Link, ETag, WWW-Authenticate +[Captures] +denied_body: body +denied_schema: header "Link" regex "<([^>]+)>" +[Asserts] +jsonpath "$.type" == "urn:sourcemeta:one:authentication-required" +jsonpath "$.title" == "Unauthorized" +jsonpath "$.status" == 401 +jsonpath "$.detail" == "This resource requires authentication" + +# The problem document validates against the error schema named by its Link +POST {{base}}/self/v1/api/schemas/evaluate{{denied_schema}} +``` +{{denied_body}} +``` +HTTP 200 +[Asserts] +jsonpath "$.valid" == true + +# The digest the instance holds is not itself a credential: presenting it is +# refused exactly as any other wrong value is +GET {{base}}/self/v1/metrics +Authorization: Bearer ed20191044553dac8f9c45e62062dd18e7dc1f898a897240b4179fb84fea3db4 +HTTP 401 +Cache-Control: no-store +Content-Type: application/problem+json +WWW-Authenticate: Bearer realm="registry" +[Asserts] +jsonpath "$.type" == "urn:sourcemeta:one:authentication-required" +jsonpath "$.status" == 401 + +# The key admits, and the exposition answers +GET {{base}}/self/v1/metrics +Authorization: Bearer local-development-only +HTTP 200 +Cache-Control: no-store +Content-Type: text/plain; version=0.0.4; charset=utf-8 +[Asserts] +header "Link" not exists +body matches /# TYPE sourcemeta_one_build_info gauge\nsourcemeta_one_build_info\{version="[^"]+",edition="enterprise"\} 1\n/ +body matches /# TYPE sourcemeta_one_http_requests_total counter\n/ +body matches /sourcemeta_one_http_requests_total\{action="metrics_v1",code="401"\} [0-9]+\n/ +body matches /# TYPE sourcemeta_one_http_request_duration_seconds histogram\n/ +body not matches /le="1e-04"/ + +# Everything else on this instance stays public, so the gate has not leaked +# past the path it names +GET {{base}}/self/v1/health +HTTP 200 +Cache-Control: no-store + +# A CORS preflight carries no credentials and is never gated +OPTIONS {{base}}/self/v1/metrics +Origin: http://example.com +Access-Control-Request-Method: GET +HTTP 204 +Cache-Control: no-store +Access-Control-Allow-Origin: * +Access-Control-Allow-Methods: GET, HEAD, OPTIONS +Access-Control-Allow-Headers: Accept, Accept-Encoding +Access-Control-Max-Age: 3600 +Allow: GET, HEAD, OPTIONS diff --git a/enterprise/e2e/public/one.json b/enterprise/e2e/public/one.json index 3d8b3ed0f..b51df19e8 100644 --- a/enterprise/e2e/public/one.json +++ b/enterprise/e2e/public/one.json @@ -11,6 +11,24 @@ "icon": "github" } }, + "authentication": [ + { + "type": "apiKey", + "algorithm": "identity", + "name": "metrics", + "paths": [ "/self/v1/metrics" ], + "keys": [ { "environmentVariable": "SOURCEMETA_ONE_METRICS_KEY" } ] + } + ], + "authentication": [ + { + "type": "apiKey", + "algorithm": "sha256", + "name": "metrics", + "paths": [ "/self/v1/metrics" ], + "keys": [ { "environmentVariable": "SOURCEMETA_ONE_METRICS_KEY" } ] + } + ], "contents": { "sourcemeta": { "title": "Sourcemeta", From f25a093513d0196dd5e1b14aaf6848f40fcfd506 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 25 Aug 2026 15:07:16 -0300 Subject: [PATCH 2/2] Fix Signed-off-by: Juan Cruz Viotti --- enterprise/e2e/public/one.json | 9 --------- 1 file changed, 9 deletions(-) diff --git a/enterprise/e2e/public/one.json b/enterprise/e2e/public/one.json index b51df19e8..e1dcda9e1 100644 --- a/enterprise/e2e/public/one.json +++ b/enterprise/e2e/public/one.json @@ -11,15 +11,6 @@ "icon": "github" } }, - "authentication": [ - { - "type": "apiKey", - "algorithm": "identity", - "name": "metrics", - "paths": [ "/self/v1/metrics" ], - "keys": [ { "environmentVariable": "SOURCEMETA_ONE_METRICS_KEY" } ] - } - ], "authentication": [ { "type": "apiKey",