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
16 changes: 16 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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' }}
Expand Down
2 changes: 2 additions & 0 deletions enterprise/e2e/public/compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,7 @@ services:
SOURCEMETA_ONE: one
environment:
- SOURCEMETA_ONE_PORT=8001
env_file:
- environment
ports:
- "${PORT}:8001"
1 change: 1 addition & 0 deletions enterprise/e2e/public/environment
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
SOURCEMETA_ONE_METRICS_KEY=ed20191044553dac8f9c45e62062dd18e7dc1f898a897240b4179fb84fea3db4
71 changes: 71 additions & 0 deletions enterprise/e2e/public/hurl/metrics.all.hurl
Original file line number Diff line number Diff line change
@@ -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: </self/v1/schemas/api/error>; 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
9 changes: 9 additions & 0 deletions enterprise/e2e/public/one.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,15 @@
"icon": "github"
}
},
"authentication": [
{
"type": "apiKey",
"algorithm": "sha256",
"name": "metrics",
"paths": [ "/self/v1/metrics" ],
"keys": [ { "environmentVariable": "SOURCEMETA_ONE_METRICS_KEY" } ]
}
],
"contents": {
"sourcemeta": {
"title": "Sourcemeta",
Expand Down
Loading