Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 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
44 changes: 43 additions & 1 deletion .github/workflows/release-candidate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,27 @@ jobs:
STATIC_METADATA_IMAGE: python:3.12-alpine
steps:
- uses: actions/checkout@v4
- name: Checkout Registry Stack Relay source
uses: actions/checkout@v4
with:
repository: registrystack/registry-stack
ref: ${{ env.REGISTRY_STACK_TAG }}
path: vendor/registry-stack
- name: Verify Registry Stack Relay source
run: |
set -euo pipefail
set -a
source versions.env
set +a
test "$REGISTRY_STACK_SOURCE_REF" = "$REGISTRY_STACK_TAG"
test "$(git -C vendor/registry-stack rev-parse HEAD)" = "$REGISTRY_STACK_SOURCE_COMMIT"
{
printf 'REGISTRY_STACK_SOURCE_COMMIT=%s\n' "$REGISTRY_STACK_SOURCE_COMMIT"
printf 'REGISTRY_RELAY_FEATURES=%s\n' "$REGISTRY_RELAY_FEATURES"
printf 'SOLMARA_RELAY_RUNTIME_IMAGE=%s\n' "$SOLMARA_RELAY_RUNTIME_IMAGE"
printf 'VOLUME_INIT_IMAGE=%s\n' "$VOLUME_INIT_IMAGE"
printf 'REGISTRY_STACK_SOURCE_DIR=%s/vendor/registry-stack\n' "$GITHUB_WORKSPACE"
} >> "$GITHUB_ENV"
- name: Checkout eSignet relay authenticator
uses: actions/checkout@v4
with:
Expand Down Expand Up @@ -96,7 +117,27 @@ jobs:
push: true
tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }}
build-args: |
REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }}
REGISTRY_RELAY_IMAGE=${{ env.SOLMARA_RELAY_RUNTIME_IMAGE }}
VOLUME_INIT_IMAGE=${{ env.VOLUME_INIT_IMAGE }}
- name: Verify hosted Relay signed bundles
env:
SOLMARA_HOSTED_RELAY_IMAGE: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }}
run: |
set -euo pipefail
for project in cra-civil nia-population sro-social mosd-programme sipf-pensions nagdi-agriculture; do
docker run --rm --platform linux/amd64 \
-e REGISTRY_RELAY_AUDIT_HASH_SECRET=0123456789abcdef0123456789abcdef \
-e REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1=abcdef0123456789abcdef0123456789 \
-e REGISTRY_RELAY_CONSULTATION_DATABASE_URL=postgresql://runtime:placeholder@postgres:5432/consultation?sslmode=require \
-e SOLMARA_NIA_DATABASE_URL=postgresql://reader:placeholder@postgres:5432/source?sslmode=require \
-e SOLMARA_SIPF_DATABASE_URL=postgresql://reader:placeholder@postgres:5432/source?sslmode=require \
"$SOLMARA_HOSTED_RELAY_IMAGE" \
config verify-bundle \
--bundle-dir "/etc/solmara/hosted-relay-bundles/$project/bundle" \
--anchor-path "/etc/solmara/hosted-relay-bundles/$project/trust-anchor.json" \
--state-path "/etc/solmara/hosted-relay-bundles/$project/antirollback-seed.json" \
| jq -e '.result == "verified" and .bundle_sequence == 1' >/dev/null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept rotated bundle sequences in release verification

When the documented bundle-rotation process regenerates the bundles with --sequence 2 or higher, this release-candidate gate rejects the valid, successfully verified bundle solely because it requires bundle_sequence == 1. Compare the reported sequence with the committed manifest for each project, or at least accept any positive verified sequence, so the workflow can publish routine configuration rotations.

Useful? React with 👍 / 👎.

done
- name: Build and push hosted Notary image
uses: docker/build-push-action@v6
with:
Expand Down Expand Up @@ -197,6 +238,7 @@ jobs:
{
echo "### Solmara image digests"
echo
echo "- ${SOLMARA_RELAY_RUNTIME_IMAGE}"
for image in solmara-lab-relay solmara-lab-notary solmara-lab-postgres solmara-lab-static-metadata solmara-lab-scenario-runner solmara-lab-home solmara-lab-portal solmara-lab-esignet-relay solmara-lab-esignet-postgres solmara-lab-esignet-ui solmara-lab-esignet-seed; do
ref="${SOLMARA_IMAGE_REGISTRY}/${image}:${SOLMARA_IMAGE_TAG}"
digest="$(docker buildx imagetools inspect "$ref" --format '{{.Manifest.Digest}}')"
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ __pycache__/
.ruff_cache/
.mypy_cache/
dist/
.cache/
projects/*/.registry-stack/
*.log
config/postgres/ssl/*
Expand Down
64 changes: 51 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@ If the exact tool version is not installed, the helper downloads the matching
release binary and verifies it against the release SHA-256 file.

`registry-projects-runtime-check` can run the compiler comparison without
starting services. `contract-generation-proof` is a separate release gate for
one bounded SRO authority pair. It compiles a harmless successor, proves the
blue pair works, rejects a mixed Relay/Notary generation before Relay execution
or source dispatch, activates the complete successor, and proves it works. Its
temporary Compose project and volumes are removed when the check finishes.
starting services. The project wrapper consumes Registryctl's versioned JSON
build report and validates its project-owned output root rather than depending
on Registryctl's private build-directory layout. `contract-generation-proof`
is a separate release gate for one bounded SRO authority pair. It compiles a
harmless successor, proves the blue pair works, rejects a mixed Relay/Notary
generation before Relay execution or source dispatch, activates the complete
successor, and proves it works. Its temporary Compose project and volumes are
removed when the check finishes.

The first wave covers three journeys:

Expand Down Expand Up @@ -75,29 +78,48 @@ just smoke-live # live Notary checks only
just portal-compose-smoke # HTTP smoke against the Compose portal and live BFF
just portal-live-e2e # browser e2e against the running local stack
just hosted-smoke # public hosted health, endpoint, scenario, and portal checks
just up # local stack with mock portal login
just up-esignet # local stack with eSignet-backed portal login
just up-dev # explicit source-built Relay development stack
just up-esignet-dev # source-built Relay development stack with eSignet
just smoke-esignet # eSignet public discovery smoke
just down # stop the local Compose topology without deleting volumes
just reset # stop the local Compose topology and delete its volumes
just up-generated # clean-checkout generation, compiler comparison, and start
just registry-projects-runtime-check # regenerate and compare all authority runtime closures
just hosted-relay-bundles-check # verify hosted Relay signatures and config closure
just registry-projects-review # complete redacted acquisition and disclosure reports
just contract-generation-proof # release-only live SRO blue/mixed/successor proof
just release-pins <registry-stack-tag> # compare committed versions.env pins against a candidate or release tag
just review # normal security and release-readiness checks
just review-release <registry-stack-tag> # candidate review with published pin validation
```

Normal startup pulls the immutable feature-enabled Relay runtime pinned in
`versions.env`; it does not clone or compile Registry Stack. The `*-dev`
recipes are the explicit source-build path. They verify the pinned source
commit and build a separate local image, leaving the standalone path unchanged.

`just generate` rewrites generated fixtures. Review those diffs like any other
committed generated artifact.

## Image Pins

`versions.env` is the root source for published image digests. The Compose
fallbacks mirror it so direct `docker compose` runs still use pinned Registry
Stack images. The current pins are the Registry Stack `v0.10.0` release digest
assets. Because that release publishes amd64 images, Compose defaults
`REGISTRY_STACK_PLATFORM` to `linux/amd64`; override it only when the release
publishes an image for another platform.
`versions.env` is the root source for published image digests and the exact
Registry Stack source commit used by Solmara's Relay runtime. The current pins
are the Registry Stack `v0.13.0` release assets. The published Relay image is
the immutable release base. The separately published Solmara Relay runtime is
built from that pinned source with the explicitly declared
`attribute-release` feature and pinned by digest in the same file. That opt-in
is required by the NIA eSignet profile because the release Relay intentionally
excludes beta API features. Normal startup pulls that runtime; the explicit
`*-dev` recipes rebuild it and refuse a dirty or mismatched source checkout.

Use `just up` rather than invoking `docker compose up` directly so the
source-pinned Relay runtime is prepared and selected. Because the release
publishes amd64 images, Compose defaults `REGISTRY_STACK_PLATFORM` to
`linux/amd64`; override it only when every selected base image is available
for another platform.

Every authority runs one Relay and one Notary. Relay consultation state and all
Notary correctness state are PostgreSQL-backed. `just gen-secrets` creates
Expand All @@ -106,6 +128,13 @@ each authority. See
[`docs/notary-postgresql-state.md`](docs/notary-postgresql-state.md) for the
database map, diagnosis, backup, recovery, and upgrade workflow.

Registry Stack v0.13.0 changes the exact retained Relay result shape. The
release-owned `REGISTRY_RELAY_STATE_EPOCH=v013` pin gives every authority a
fresh consultation database and role set, so v0.10.0 and v0.13.0 Relay
binaries cannot share one state plane. Do not override it to the old,
unsuffixed database names. The PostgreSQL runbook describes the stopped-writer
cutover and rollback boundary.

## Hosted Deployment

See [`docs/hosted-deployment.md`](docs/hosted-deployment.md) for the full
Expand Down Expand Up @@ -133,6 +162,12 @@ authority-owned PostgreSQL state, persistent Relay snapshot caches, and
workload credentials. Notary containers do not use Redis or a writable state
directory.

Hosted Relay starts from instance-bound signed Config Bundles. The wrapper
contains only the public trust anchors and signed closures; the offline signing
key is not committed. A sequence-zero baseline is copied only when a Relay
state volume is empty, allowing first boot while keeping later bundle sequence
rollback protection durable in that volume.

Run `just registry-projects-sync` after editing an authority project, then
`just registry-projects-runtime-check` to verify the local and hosted Relay and
Notary closures are deterministic.
Expand All @@ -144,8 +179,11 @@ refusals, the Visitor Center scenario proxy, and the portal live BFF. Add
`SOLMARA_HOSTED_SMOKE_BROWSER=1` when you also want hosted Playwright coverage
for the Visitor Center and portal.

The `release-candidate` workflow builds digest-pinned Solmara-owned images for
the hosted wrappers and app services, then writes the digest refs to the
The `release-candidate` workflow verifies the pinned Registry Stack source and
uses the published feature-enabled Relay runtime digest as the base for the
hosted Relay wrapper. The runtime is reviewed and published separately, so a
Solmara candidate does not recompile it. The workflow builds the Solmara-owned
images and writes their digest refs, together with the runtime pin, to the
workflow summary for Coolify env vars:
`SOLMARA_RELAY_IMAGE`, `SOLMARA_NOTARY_IMAGE`, `SOLMARA_POSTGRES_IMAGE`,
`SOLMARA_STATIC_METADATA_IMAGE`, `SOLMARA_HOME_IMAGE`,
Expand Down
42 changes: 34 additions & 8 deletions compose.coolify.agriculture.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ x-relay-bootstrap-common: &relay-bootstrap-common
platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64}
restart: "no"

x-relay-config-state-init: &relay-config-state-init
image: ${SOLMARA_RELAY_IMAGE:?set digest-pinned Solmara relay image}
platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64}
user: "0:0"
restart: "no"
entrypoint: ["/bin/busybox", "sh", "-eu", "-c"]

x-notary-common: &notary-common
image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image}
platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64}
Expand Down Expand Up @@ -54,6 +61,7 @@ services:
POSTGRES_PASSWORD: ${SOLMARA_POSTGRES_PASSWORD}
POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab}
SOLMARA_SOURCE_READER_DATABASES: ""
REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v013}
SOLMARA_RELAY_DATABASES: nagdi
SOLMARA_NOTARY_DATABASES: nagdi
NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD: ${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}
Expand All @@ -64,7 +72,7 @@ services:
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 10
Expand All @@ -82,29 +90,47 @@ services:
postgres:
condition: service_healthy

nagdi-relay-config-state-init:
<<: *relay-config-state-init
command:
- |
/bin/busybox mkdir -p /var/lib/registry-relay/cache
/bin/busybox chown 65532:65532 /var/lib/registry-relay/cache
if ! /bin/busybox test -e /var/lib/registry-relay/cache/config-bundle-antirollback.json; then
/bin/busybox cp /etc/solmara/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json /var/lib/registry-relay/cache/config-bundle-antirollback.json
/bin/busybox chown 65532:65532 /var/lib/registry-relay/cache/config-bundle-antirollback.json
/bin/busybox chmod 0600 /var/lib/registry-relay/cache/config-bundle-antirollback.json
fi
volumes:
- nagdi-relay-cache:/var/lib/registry-relay/cache

nagdi-relay-state-bootstrap:
<<: *relay-bootstrap-common
command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nagdi_consultation_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"]
command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_owner", "--keyring-maintenance-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL", "--keyring-reader-database-url-env", "REGISTRY_RELAY_STATE_KEYRING_READER_URL", "--active-key-id", "epoch-1", "--active-write-deadline-unix-ms", "${SOLMARA_RELAY_KEY_ACTIVE_WRITE_DEADLINE_UNIX_MS}", "--audit-event-retention-ms", "${SOLMARA_RELAY_AUDIT_EVENT_RETENTION_MS}"]
environment:
<<: *relay-environment
REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET}
REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1}
REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation?sslmode=require"
REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation?sslmode=require"
REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nagdi_consultation_keyring_maintenance:${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation?sslmode=require"
REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nagdi_consultation_keyring_reader:${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation?sslmode=require"
REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require"
REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require"
REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${NAGDI_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require"
REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${NAGDI_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require"
volumes:
- nagdi-relay-cache:/var/lib/registry-relay/cache
depends_on:
nagdi-relay-config-state-init:
condition: service_completed_successfully
registry-postgresql-bootstrap:
condition: service_completed_successfully

nagdi-agriculture-relay:
<<: *relay-common
command: ["--config", "/etc/solmara/registry-projects/hosted/nagdi-agriculture/relay/relay.yaml"]
command: ["--config", "/etc/solmara/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml"]
environment:
<<: *relay-environment
REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NAGDI_RELAY_AUDIT_HASH_SECRET}
REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NAGDI_RELAY_AUDIT_PSEUDONYM_EPOCH_1}
REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation?sslmode=require"
REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NAGDI_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nagdi_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require"
expose: ["8080", "8081"]
volumes:
- nagdi-relay-cache:/var/lib/registry-relay/cache
Expand Down
Loading
Loading