From 59881a6b1f2b469634945142b41d0e8e4c7defec Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 21:38:25 +0700 Subject: [PATCH 01/14] Adopt Registry Stack v0.13.0 Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 38 ++++- .gitignore | 1 + README.md | 36 +++-- compose.coolify.agriculture.yaml | 15 +- compose.coolify.interior.yaml | 27 ++-- compose.coolify.labour-pensions.yaml | 15 +- compose.coolify.social-development.yaml | 27 ++-- compose.yaml | 93 +++++------ docker/notary/Dockerfile | 2 +- docker/relay-runtime/Dockerfile | 29 ++++ docker/relay/Dockerfile | 2 +- docs/changelog.md | 26 ++++ docs/hosted-deployment.md | 13 +- docs/notary-postgresql-state.md | 43 +++++- justfile | 12 +- .../nagdi-agriculture/notary/notary.yaml | 6 - .../hosted/nia-population/notary/notary.yaml | 1 - .../hosted/sipf-pensions/notary/notary.yaml | 1 - .../nagdi-agriculture/notary/notary.yaml | 6 - .../local/nia-population/notary/notary.yaml | 1 - .../local/sipf-pensions/notary/notary.yaml | 1 - scenario-runner/test_server.py | 12 ++ scenarios/citizen.py | 2 - scenarios/farmer_voucher.py | 7 +- scenarios/pension_survivor.py | 2 - scripts/build-relay-runtime.sh | 93 +++++++++++ scripts/check-fiction.sh | 1 + scripts/check-nid-aliases.py | 1 + scripts/check-release-pins.py | 33 +++- scripts/contract-generation-proof.py | 9 +- scripts/init-notary-postgresql.sh | 10 +- scripts/registry-projects.sh | 33 ++-- scripts/review.sh | 2 +- scripts/smoke-nia-attribute-release.py | 99 ++++++++++++ scripts/smoke.sh | 13 ++ scripts/test_quality_scripts.py | 144 ++++++++++++++++++ scripts/test_release_pins.py | 74 ++++++++- scripts/test_smoke_nia_attribute_release.py | 109 +++++++++++++ versions.env | 11 +- 39 files changed, 896 insertions(+), 154 deletions(-) create mode 100644 docker/relay-runtime/Dockerfile create mode 100755 scripts/build-relay-runtime.sh create mode 100755 scripts/smoke-nia-attribute-release.py create mode 100644 scripts/test_smoke_nia_attribute_release.py diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 94e53bb..4ed2276 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -37,6 +37,25 @@ 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 'REGISTRY_STACK_SOURCE_DIR=%s/vendor/registry-stack\n' "$GITHUB_WORKSPACE" + } >> "$GITHUB_ENV" - name: Checkout eSignet relay authenticator uses: actions/checkout@v4 with: @@ -87,6 +106,21 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Prepare feature-enabled Relay runtime build + run: cp docker/relay-runtime/Dockerfile vendor/registry-stack/Dockerfile.solmara-relay-runtime + - name: Build and push feature-enabled Relay runtime + id: relay-runtime + uses: docker/build-push-action@v6 + with: + context: vendor/registry-stack + file: vendor/registry-stack/Dockerfile.solmara-relay-runtime + platforms: linux/amd64 + push: true + tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay-runtime:${{ env.SOLMARA_IMAGE_TAG }} + build-args: | + REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }} + REGISTRY_STACK_SOURCE_COMMIT=${{ env.REGISTRY_STACK_SOURCE_COMMIT }} + REGISTRY_RELAY_FEATURES=${{ env.REGISTRY_RELAY_FEATURES }} - name: Build and push hosted Relay image uses: docker/build-push-action@v6 with: @@ -96,7 +130,7 @@ 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_IMAGE_REGISTRY }}/solmara-lab-relay-runtime@${{ steps.relay-runtime.outputs.digest }} - name: Build and push hosted Notary image uses: docker/build-push-action@v6 with: @@ -197,7 +231,7 @@ jobs: { echo "### Solmara image digests" echo - 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 + for image in solmara-lab-relay-runtime 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}}')" echo "- ${SOLMARA_IMAGE_REGISTRY}/${image}@${digest}" diff --git a/.gitignore b/.gitignore index f996cdf..264ab1e 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ __pycache__/ .ruff_cache/ .mypy_cache/ dist/ +.cache/ projects/*/.registry-stack/ *.log config/postgres/ssl/* diff --git a/README.md b/README.md index e08aa0f..c70454f 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ 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 registry-projects-review # complete redacted acquisition and disclosure reports just contract-generation-proof # release-only live SRO blue/mixed/successor proof just release-pins # compare committed versions.env pins against a candidate or release tag just review # normal security and release-readiness checks @@ -92,12 +93,20 @@ 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 runtime base, while `just up` builds and caches a replacement +Relay binary from the pinned source with the explicitly declared +`attribute-release` feature. That opt-in is required by the NIA eSignet +profile because the release Relay intentionally excludes beta API features. +The build refuses 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 @@ -106,6 +115,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 @@ -144,9 +160,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 -workflow summary for Coolify env vars: +The `release-candidate` workflow verifies the pinned Registry Stack source, +builds and publishes the feature-enabled Relay runtime, then uses its immutable +digest as the base for the hosted Relay wrapper. It also builds the other +Solmara-owned images and writes their digest refs to the workflow summary for +Coolify env vars: `SOLMARA_RELAY_IMAGE`, `SOLMARA_NOTARY_IMAGE`, `SOLMARA_POSTGRES_IMAGE`, `SOLMARA_STATIC_METADATA_IMAGE`, `SOLMARA_HOME_IMAGE`, `SOLMARA_PORTAL_IMAGE`, `SOLMARA_SCENARIO_RUNNER_IMAGE`, diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index 4484635..8531ff9 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -54,6 +54,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} @@ -64,7 +65,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 @@ -84,15 +85,15 @@ services: 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/registry-projects/hosted/nagdi-agriculture/relay/relay.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" depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully @@ -104,7 +105,7 @@ services: <<: *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 diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 820c064..df04d86 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -55,6 +55,7 @@ services: POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} SOLMARA_SOURCE_READER_DATABASES: nia NIA_SOURCE_POSTGRES_READER_PASSWORD: ${NIA_SOURCE_POSTGRES_READER_PASSWORD} + REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v013} SOLMARA_RELAY_DATABASES: cra nia SOLMARA_NOTARY_DATABASES: cra nia CRA_RELAY_POSTGRES_RUNTIME_PASSWORD: ${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD} @@ -70,7 +71,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 @@ -90,30 +91,30 @@ services: cra-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/cra-civil/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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/registry-projects/hosted/cra-civil/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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: ${CRA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_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_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully nia-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/nia-population/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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/registry-projects/hosted/nia-population/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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: ${NIA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_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_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} depends_on: registry-postgresql-bootstrap: @@ -126,7 +127,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" expose: ["8080", "8081"] volumes: - cra-relay-cache:/var/lib/registry-relay/cache @@ -143,7 +144,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt expose: ["8080", "8081"] diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index d297b7b..846ea65 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -55,6 +55,7 @@ services: POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} SOLMARA_SOURCE_READER_DATABASES: sipf SIPF_SOURCE_POSTGRES_READER_PASSWORD: ${SIPF_SOURCE_POSTGRES_READER_PASSWORD} + REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v013} SOLMARA_RELAY_DATABASES: sipf SOLMARA_NOTARY_DATABASES: sipf SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD} @@ -65,7 +66,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 @@ -85,15 +86,15 @@ services: sipf-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/sipf-pensions/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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/registry-projects/hosted/sipf-pensions/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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: ${SIPF_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_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_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} depends_on: registry-postgresql-bootstrap: @@ -106,7 +107,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt expose: ["8080", "8081"] diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 525489e..6c469f4 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -54,6 +54,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: sro programme SOLMARA_NOTARY_DATABASES: sro programme SRO_RELAY_POSTGRES_RUNTIME_PASSWORD: ${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD} @@ -69,7 +70,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 @@ -89,30 +90,30 @@ services: sro-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/sro-social/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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/registry-projects/hosted/sro-social/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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: ${SRO_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_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_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully programme-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/mosd-programme/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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/registry-projects/hosted/mosd-programme/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_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_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully @@ -124,7 +125,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" expose: ["8080", "8081"] volumes: - sro-relay-cache:/var/lib/registry-relay/cache @@ -141,7 +142,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" expose: ["8080", "8081"] volumes: - programme-relay-cache:/var/lib/registry-relay/cache diff --git a/compose.yaml b/compose.yaml index 4bababa..6cb88e3 100644 --- a/compose.yaml +++ b/compose.yaml @@ -8,7 +8,7 @@ x-notary-environment: ¬ary-environment REGISTRY_NOTARY_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt x-relay-common: &relay-common - image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:81d0b97e60560159a0f77ee10008a859f4e5dc15fc70fcb2c2fb50f8f190617f} + image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:00681003d06667cde377e15640a3825eec97aca26f486cde2e9f5013ef56819e} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: unless-stopped environment: *relay-environment @@ -19,7 +19,7 @@ x-relay-common: &relay-common retries: 3 x-notary-common: ¬ary-common - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} user: "65534:65534" restart: unless-stopped @@ -38,7 +38,7 @@ x-notary-common: ¬ary-common retries: 3 x-relay-bootstrap-common: &relay-bootstrap-common - image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:81d0b97e60560159a0f77ee10008a859f4e5dc15fc70fcb2c2fb50f8f190617f} + image: ${REGISTRY_RELAY_IMAGE:-ghcr.io/registrystack/registry-relay@sha256:00681003d06667cde377e15640a3825eec97aca26f486cde2e9f5013ef56819e} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" @@ -73,6 +73,7 @@ services: POSTGRES_DB: ${SOLMARA_POSTGRES_DB:-solmara_lab} NIA_SOURCE_POSTGRES_READER_PASSWORD: ${NIA_SOURCE_POSTGRES_READER_PASSWORD} SIPF_SOURCE_POSTGRES_READER_PASSWORD: ${SIPF_SOURCE_POSTGRES_READER_PASSWORD} + REGISTRY_RELAY_STATE_EPOCH: ${REGISTRY_RELAY_STATE_EPOCH:-v013} SOLMARA_RELAY_DATABASES: cra nia sro programme sipf nagdi SOLMARA_NOTARY_DATABASES: cra nia sro programme sipf nagdi SOLMARA_SOURCE_READER_DATABASES: nia sipf @@ -122,7 +123,7 @@ services: - ./scripts/postgres-entrypoint.sh:/usr/local/bin/solmara-postgres-entrypoint.sh:ro - ./config/postgres/ssl:/run/solmara-postgres-ssl:ro 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 @@ -249,15 +250,15 @@ services: cra-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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: ${CRA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_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_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" volumes: - ./runtime/registry-projects/local/cra-civil/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro @@ -267,15 +268,15 @@ services: nia-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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: ${NIA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_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_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} volumes: - ./runtime/registry-projects/local/nia-population/relay:/etc/registry-relay:ro @@ -286,15 +287,15 @@ services: sro-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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: ${SRO_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_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_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" volumes: - ./runtime/registry-projects/local/sro-social/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro @@ -304,15 +305,15 @@ services: programme-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_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_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" volumes: - ./runtime/registry-projects/local/mosd-programme/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro @@ -322,15 +323,15 @@ services: sipf-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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/registry-relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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: ${SIPF_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" - REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_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_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} volumes: - ./runtime/registry-projects/local/sipf-pensions/relay:/etc/registry-relay:ro @@ -341,15 +342,15 @@ services: nagdi-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/registry-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/registry-relay/relay.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: - ./runtime/registry-projects/local/nagdi-agriculture/relay:/etc/registry-relay:ro - ./config/postgres/ssl/server.crt:/etc/solmara/postgres/root.crt:ro @@ -364,7 +365,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${CRA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${CRA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" ports: - "${SOLMARA_CRA_RELAY_PORT:-4311}:8080" - "${SOLMARA_CRA_NOTARY_PORT:-4325}:8081" @@ -387,7 +388,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${NIA_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${NIA_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt ports: @@ -411,7 +412,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SRO_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SRO_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" ports: - "${SOLMARA_SRO_RELAY_PORT:-4313}:8080" - "${SOLMARA_SRO_NOTARY_PORT:-4327}:8081" @@ -434,7 +435,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${PROGRAMME_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${PROGRAMME_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" ports: - "${SOLMARA_PROGRAMME_RELAY_PORT:-4314}:8080" - "${SOLMARA_PROGRAMME_NOTARY_PORT:-4328}:8081" @@ -457,7 +458,7 @@ services: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1: ${SIPF_RELAY_AUDIT_PSEUDONYM_EPOCH_1} - REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation?sslmode=require" + REGISTRY_RELAY_CONSULTATION_DATABASE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_runtime:${SIPF_RELAY_POSTGRES_RUNTIME_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} DATA_GATE_POSTGRES_ROOT_CERT_PATH: /etc/solmara/postgres/root.crt ports: @@ -481,7 +482,7 @@ services: <<: *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" ports: - "${SOLMARA_NAGDI_RELAY_PORT:-4316}:8080" - "${SOLMARA_NAGDI_NOTARY_PORT:-4323}:8081" @@ -608,7 +609,7 @@ services: condition: service_completed_successfully cra-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_cra_owner", "--runtime-role", "solmara_notary_cra_runtime"] @@ -649,7 +650,7 @@ services: condition: service_completed_successfully nia-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nia_owner", "--runtime-role", "solmara_notary_nia_runtime"] @@ -688,7 +689,7 @@ services: condition: service_completed_successfully sro-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sro_owner", "--runtime-role", "solmara_notary_sro_runtime"] @@ -727,7 +728,7 @@ services: condition: service_completed_successfully programme-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_programme_owner", "--runtime-role", "solmara_notary_programme_runtime"] @@ -802,7 +803,7 @@ services: condition: service_completed_successfully sipf-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_sipf_owner", "--runtime-role", "solmara_notary_sipf_runtime"] @@ -842,7 +843,7 @@ services: condition: service_completed_successfully nagdi-notary-state-install: - image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639} + image: ${REGISTRY_NOTARY_IMAGE:-ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} restart: "no" command: ["--config", "/etc/registry-notary/notary.yaml", "state", "install", "--migration-url-env", "REGISTRY_NOTARY_POSTGRES_MIGRATOR_URL", "--owner-role", "solmara_notary_nagdi_owner", "--runtime-role", "solmara_notary_nagdi_runtime"] diff --git a/docker/notary/Dockerfile b/docker/notary/Dockerfile index 684f470..51b6032 100644 --- a/docker/notary/Dockerfile +++ b/docker/notary/Dockerfile @@ -1,4 +1,4 @@ -ARG REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639 +ARG REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297 FROM ${REGISTRY_NOTARY_IMAGE} COPY runtime/registry-projects/hosted/cra-civil/notary/notary.yaml /etc/solmara/registry-projects/hosted/cra-civil/notary/notary.yaml diff --git a/docker/relay-runtime/Dockerfile b/docker/relay-runtime/Dockerfile new file mode 100644 index 0000000..0b453e0 --- /dev/null +++ b/docker/relay-runtime/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.7 + +ARG REGISTRY_RELAY_IMAGE + +FROM rust:1.95-trixie@sha256:f49565f188ee00bc2a18dd418183f2c5f23ef7d6e691890517ed341a598f67c3 AS builder +WORKDIR /workspace/registry-stack + +COPY . . + +ARG REGISTRY_RELAY_FEATURES +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/workspace/registry-stack/target \ + cargo build --release --locked \ + --package registry-relay \ + --features "${REGISTRY_RELAY_FEATURES}" \ + && cp target/release/registry-relay /usr/local/bin/registry-relay \ + && cp target/release/registry-relay-rhai-worker /usr/local/bin/registry-relay-rhai-worker + +FROM ${REGISTRY_RELAY_IMAGE} + +ARG REGISTRY_RELAY_IMAGE +ARG REGISTRY_STACK_SOURCE_COMMIT +ARG REGISTRY_RELAY_FEATURES +LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}" +LABEL org.opencontainers.image.revision="${REGISTRY_STACK_SOURCE_COMMIT}" +LABEL org.registrystack.solmara.relay-features="${REGISTRY_RELAY_FEATURES}" + +COPY --from=builder /usr/local/bin/registry-relay /usr/local/bin/registry-relay +COPY --from=builder /usr/local/bin/registry-relay-rhai-worker /usr/local/bin/registry-relay-rhai-worker diff --git a/docker/relay/Dockerfile b/docker/relay/Dockerfile index e36713a..709c470 100644 --- a/docker/relay/Dockerfile +++ b/docker/relay/Dockerfile @@ -1,4 +1,4 @@ -ARG REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:81d0b97e60560159a0f77ee10008a859f4e5dc15fc70fcb2c2fb50f8f190617f +ARG REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:00681003d06667cde377e15640a3825eec97aca26f486cde2e9f5013ef56819e FROM ${REGISTRY_RELAY_IMAGE} COPY ministries /var/lib/solmara/ministries diff --git a/docs/changelog.md b/docs/changelog.md index eeaa041..a3bd2a4 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -3,6 +3,32 @@ A small dated log of what changed in the visitor center and the lab topology. Newest entry first. +## 2026-07-25 Registry Stack v0.13.0 adoption + +The lab now pins the Registry Stack `v0.13.0` Relay and Notary image digests +and the matching Registryctl binary. All six authored projects pass the +v0.13.0 compiler checks and fixture suites. Regenerated Notary configuration +keeps SD-JWT under credential issuance profiles while removing it from claim +evaluation formats, which now advertise only the canonical claim-result +format. + +Relay consultation state uses the release-owned `v013` database epoch. The +idempotent PostgreSQL bootstrap creates new per-authority databases and roles +on fresh or existing clusters, preventing old and v0.13.0 Relay binaries from +sharing the retained result state plane. `registry-projects-review` exposes +Registryctl's complete redacted acquisition and disclosure explanation when a +reviewer needs more detail than the concise default report. Release-pin review +now also rejects a Registryctl version that does not match the selected +Registry Stack tag. + +The NIA eSignet profile remains explicit beta functionality. Solmara now +builds its Relay runtime from the exact v0.13.0 source commit with only the +required `attribute-release` feature, using the published Relay image as its +runtime base. The release-candidate workflow publishes that source-bound +runtime before the hosted wrapper. PostgreSQL health checks now probe the TCP +server, preventing bootstrap jobs from racing PostgreSQL's temporary +initialization server on a fresh volume. + ## 2026-07-16 Registry Stack v0.10.0 adoption The lab now uses the authenticated Relay and Notary image digests and diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index dd1d646..5968774 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -74,7 +74,11 @@ The other public endpoints are: Registry Stack Relay and Notary image refs are inputs to the Solmara wrapper builds. The `release-candidate` workflow requires a Registry Stack candidate or release tag and accepts only Relay and Notary input digests that match the -committed `versions.env` pins. It builds the deployable Solmara images and +committed `versions.env` pins. It also checks out the exact Registry Stack +source commit declared there and builds a Relay runtime with the +`attribute-release` feature required by the NIA eSignet profile. The published +release Relay remains the immutable runtime base. The feature runtime digest +then becomes the base of the deployable Solmara Relay wrapper. The workflow reports immutable digest refs for these Coolify variables: - `SOLMARA_RELAY_IMAGE` @@ -218,6 +222,13 @@ The bootstrap container creates only the authority keys listed in Notary receive runtime credentials only. Schema installation uses dedicated jobs and never gives migration credentials to a serving process. +Registry Stack v0.13.0 uses the `v013` Relay state epoch from `versions.env`. +On an existing cluster, the idempotent bootstrap creates a separate +per-authority Relay database and role set before the v0.13.0 Relay starts. +Quiesce the old Relay writers first and do not reuse their unsuffixed +consultation databases. Follow the complete stopped-writer and rollback +procedure in [`notary-postgresql-state.md`](notary-postgresql-state.md). + Back up, restore, and upgrade each Notary database independently. See [`notary-postgresql-state.md`](notary-postgresql-state.md) for the database map and recovery sequence. Preserve the eSignet Redis volume separately because it diff --git a/docs/notary-postgresql-state.md b/docs/notary-postgresql-state.md index 81295d7..3a4d5c7 100644 --- a/docs/notary-postgresql-state.md +++ b/docs/notary-postgresql-state.md @@ -5,14 +5,14 @@ Notary owns an independent PostgreSQL database and role set. PostgreSQL servers may be shared within a local or hosted Compose application, but databases, owners, migrators, and runtime roles are never shared between Notaries. -| Authority | Relay service | Notary service | Database | Local Relay / Notary | -|---|---|---|---|---| -| Civil Registration Authority (CRA) | `cra-civil-relay` | `cra-notary` | `solmara_notary_cra` | `4311` / `4325` | -| National Identity Agency (NIA) | `nia-population-relay` | `nia-notary` | `solmara_notary_nia` | `4312` / `4326` | -| Social Registry Office (SRO) | `sro-social-relay` | `sro-notary` | `solmara_notary_sro` | `4313` / `4327` | -| Programme MIS | `programme-mis-relay` | `programme-notary` | `solmara_notary_programme` | `4314` / `4328` | -| Social Insurance and Pensions Fund (SIPF) | `sipf-pensions-relay` | `sipf-notary` | `solmara_notary_sipf` | `4315` / `4322` | -| National Agricultural Data Institute (NAgDI) | `nagdi-agriculture-relay` | `nagdi-notary` | `solmara_notary_nagdi` | `4316` / `4323` | +| Authority | Relay service | Notary service | Relay state database | Notary database | Local Relay / Notary | +|---|---|---|---|---|---| +| Civil Registration Authority (CRA) | `cra-civil-relay` | `cra-notary` | `solmara_relay_cra_consultation_v013` | `solmara_notary_cra` | `4311` / `4325` | +| National Identity Agency (NIA) | `nia-population-relay` | `nia-notary` | `solmara_relay_nia_consultation_v013` | `solmara_notary_nia` | `4312` / `4326` | +| Social Registry Office (SRO) | `sro-social-relay` | `sro-notary` | `solmara_relay_sro_consultation_v013` | `solmara_notary_sro` | `4313` / `4327` | +| Programme MIS | `programme-mis-relay` | `programme-notary` | `solmara_relay_programme_consultation_v013` | `solmara_notary_programme` | `4314` / `4328` | +| Social Insurance and Pensions Fund (SIPF) | `sipf-pensions-relay` | `sipf-notary` | `solmara_relay_sipf_consultation_v013` | `solmara_notary_sipf` | `4315` / `4322` | +| National Agricultural Data Institute (NAgDI) | `nagdi-agriculture-relay` | `nagdi-notary` | `solmara_relay_nagdi_consultation_v013` | `solmara_notary_nagdi` | `4316` / `4323` | The local topology shares one PostgreSQL server for developer convenience. Hosted authority applications keep the same database boundaries within their @@ -106,6 +106,33 @@ upgrade cannot be completed, restore the pre-upgrade database and the matching image and configuration together. The normative product procedure is the [Registry Notary PostgreSQL state operations guide](https://github.com/registrystack/registry-stack/blob/main/products/notary/docs/postgresql-state-operations.md). +### Registry Stack v0.13.0 cutover + +Registry Stack v0.13.0 removes `provenance.consent` from the exact retained +Relay result contract. Old and v0.13.0 Relay binaries must not share a +consultation state plane. Solmara enforces that boundary with +`REGISTRY_RELAY_STATE_EPOCH=v013` in `versions.env`. The PostgreSQL bootstrap +uses the epoch in every Relay database and role name and runs idempotently on +fresh and existing clusters. + +For the v0.10.0 to v0.13.0 cutover: + +1. Stop new authority traffic and stop every old Relay and Notary writer. +2. Drain retained terminal replay lifetimes, then back up every old Relay and + Notary database with its exact image and configuration refs. +3. Deploy the v0.13.0 Compose closure. The bootstrap creates the new `v013` + Relay databases and roles, and each Relay bootstraps its empty state plane. + The Notary installers migrate or attest the existing Notary databases. +4. Require every Relay and Notary `/ready` check, `state doctor`, the complete + local or hosted smoke, and the PostgreSQL restart-persistence proof before + reopening traffic. + +Keep the old, unsuffixed Relay databases quiesced until the rollback window +closes. A rollback restores the pre-upgrade Notary backups and matching +v0.10.0 images and configs, then reconnects the old Relay binaries only to the +old Relay databases. Never point a v0.13.0 Relay at an unsuffixed database or +an old Relay at a `v013` database. + The local topology deliberately pins PostgreSQL 16 and its `/var/lib/postgresql/data` mount layout. Do not change the image tag to 18 in place. PostgreSQL 18's official container layout mounts the parent diff --git a/justfile b/justfile index b582a77..a57951b 100644 --- a/justfile +++ b/justfile @@ -28,6 +28,10 @@ up-generated: registry-projects-check: scripts/registry-projects.sh check +# Print the complete redacted acquisition and disclosure plan for every authority. +registry-projects-review: + scripts/registry-projects.sh review + # Run every synthetic authority integration fixture offline. registry-projects-test: scripts/registry-projects.sh test @@ -93,7 +97,8 @@ compose: # Start the local topology. up: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build + scripts/build-relay-runtime.sh + @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build # Stop the local topology without removing local volumes. down: @@ -101,7 +106,8 @@ down: # Start the local topology with eSignet-backed portal login. up-esignet: - @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build + scripts/build-relay-runtime.sh + @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build # Stop the local eSignet topology without removing local volumes. down-esignet: @@ -151,7 +157,7 @@ home-live-e2e: hosted-smoke *args: uv run scripts/smoke-hosted.py {{args}} -# Verify committed Registry Stack image pins match a candidate or release tag. +# Verify committed Registry Stack release inputs match a candidate or release tag. release-pins tag: scripts/check-release-pins.py "$1" diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml index 33ad156..527ec4e 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml +++ b/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml @@ -52,7 +52,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: eligible-for-livestock-movement-permit purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -107,7 +106,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: origin-district-not-quarantined-for-species purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -157,7 +155,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: registered-herd purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -216,7 +213,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: data-use-authorized-for-purpose purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review @@ -275,7 +271,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: eligible-for-climate-smart-input-voucher purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review @@ -342,7 +337,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: farmer-registered purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review diff --git a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml b/runtime/registry-projects/hosted/nia-population/notary/notary.yaml index 7af6915..baa86ef 100644 --- a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml +++ b/runtime/registry-projects/hosted/nia-population/notary/notary.yaml @@ -92,7 +92,6 @@ evidence: id: solmara-nia-population.nia-citizen-status.population type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: citizen-population-record-active purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service diff --git a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml b/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml index 30b0717..055000d 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml +++ b/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml @@ -78,7 +78,6 @@ evidence: id: solmara-sipf-pensions.sipf-survivor-benefit.survivor type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: survivor-is-eligible purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination diff --git a/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml b/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml index 7931605..d4f45e9 100644 --- a/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml +++ b/runtime/registry-projects/local/nagdi-agriculture/notary/notary.yaml @@ -52,7 +52,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: eligible-for-livestock-movement-permit purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -107,7 +106,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: origin-district-not-quarantined-for-species purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -157,7 +155,6 @@ evidence: id: solmara-nagdi-agriculture.livestock.herd type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: registered-herd purpose: https://id.registrystack.org/solmara/purpose/livestock-movement-control @@ -216,7 +213,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: data-use-authorized-for-purpose purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review @@ -275,7 +271,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: eligible-for-climate-smart-input-voucher purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review @@ -342,7 +337,6 @@ evidence: id: solmara-nagdi-agriculture.voucher.farmer type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: farmer-registered purpose: https://id.registrystack.org/solmara/purpose/voucher-eligibility-review diff --git a/runtime/registry-projects/local/nia-population/notary/notary.yaml b/runtime/registry-projects/local/nia-population/notary/notary.yaml index 751fd11..48e9cf1 100644 --- a/runtime/registry-projects/local/nia-population/notary/notary.yaml +++ b/runtime/registry-projects/local/nia-population/notary/notary.yaml @@ -92,7 +92,6 @@ evidence: id: solmara-nia-population.nia-citizen-status.population type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: citizen-population-record-active purpose: https://id.registrystack.org/solmara/purpose/citizen-self-service diff --git a/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml b/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml index 0b5994a..5da8438 100644 --- a/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml +++ b/runtime/registry-projects/local/sipf-pensions/notary/notary.yaml @@ -78,7 +78,6 @@ evidence: id: solmara-sipf-pensions.sipf-survivor-benefit.survivor type: registry_backed formats: - - application/dc+sd-jwt - application/vnd.registry-notary.claim-result+json id: survivor-is-eligible purpose: https://id.registrystack.org/solmara/purpose/survivor-benefit-determination diff --git a/scenario-runner/test_server.py b/scenario-runner/test_server.py index dc400c8..46f1cbb 100644 --- a/scenario-runner/test_server.py +++ b/scenario-runner/test_server.py @@ -299,6 +299,10 @@ def fake_credential_attempt( ) self.assertEqual(captured["claim_ids"], ["survivor-is-eligible"]) self.assertTrue(captured["url"].endswith("/v1/credentials")) + self.assertEqual( + result["request_source"]["body"]["format"], + common.CLAIM_RESULT_FORMAT, + ) self.assertEqual(result["credential"]["status"], "issued") def test_farmer_voucher_purpose_override_reaches_request_source(self) -> None: @@ -310,6 +314,10 @@ def test_farmer_voucher_purpose_override_reaches_request_source(self) -> None: self.assertEqual( payload["result"]["request_source"]["headers"]["Data-Purpose"], purpose ) + self.assertEqual( + payload["result"]["request_source"]["body"]["format"], + common.CLAIM_RESULT_FORMAT, + ) def test_farmer_voucher_denial_step_ignores_override(self) -> None: payload = self.post_json( @@ -408,6 +416,10 @@ def fake_credential_attempt( [body["claims"] for _, body in calls], [["civil-record-linked"], ["citizen-population-record-active"]], ) + self.assertEqual( + [body["format"] for _, body in calls], + [common.CLAIM_RESULT_FORMAT, common.CLAIM_RESULT_FORMAT], + ) self.assertEqual( [trace["service_id"] for trace in result["source_trace"]], ["cra-notary", "nia-notary"], diff --git a/scenarios/citizen.py b/scenarios/citizen.py index dff28a3..2ff2593 100644 --- a/scenarios/citizen.py +++ b/scenarios/citizen.py @@ -8,7 +8,6 @@ from .common import ( CLAIM_RESULT_FORMAT, PURPOSES, - SD_JWT_VC_FORMAT, StepHttpResult, auth_headers, credential_attempt, @@ -204,7 +203,6 @@ def authority_requests( purpose, subject, NIA_CLAIMS, - SD_JWT_VC_FORMAT, ), ] return [] diff --git a/scenarios/farmer_voucher.py b/scenarios/farmer_voucher.py index e043512..a2db5d2 100644 --- a/scenarios/farmer_voucher.py +++ b/scenarios/farmer_voucher.py @@ -5,7 +5,7 @@ from typing import Any -from .common import CLAIM_RESULT_FORMAT, PURPOSES, SD_JWT_VC_FORMAT, auth_headers, credential_attempt, evaluation_body, friendly_result, http_json, missing_runtime_token, request_source, source_response, standard_error_result +from .common import CLAIM_RESULT_FORMAT, PURPOSES, auth_headers, credential_attempt, evaluation_body, friendly_result, http_json, missing_runtime_token, request_source, source_response, standard_error_result from .service_config import service_token, service_token_env, service_url @@ -107,9 +107,8 @@ def _request(config: dict[str, Any], step_id: str, *, send: bool) -> dict[str, A purpose = request_purpose(config, step_id) token = service_token(SERVICE_ID) credential_profile = credential_profile_for_step(step_id) - evaluation_format = SD_JWT_VC_FORMAT if step_id in CREDENTIAL_STEPS else CLAIM_RESULT_FORMAT - headers = auth_headers(token, purpose, evaluation_format if step_id != "discover" else "application/json") - body = None if step_id == "discover" else evaluation_body(subject or "", claims, scheme="farmer_id", format=evaluation_format) + headers = auth_headers(token, purpose, CLAIM_RESULT_FORMAT if step_id != "discover" else "application/json") + body = None if step_id == "discover" else evaluation_body(subject or "", claims, scheme="farmer_id") if step_id != "discover" and not subject: return standard_error_result(step_id) request = request_source("GET" if step_id == "discover" else "POST", url, headers, body) diff --git a/scenarios/pension_survivor.py b/scenarios/pension_survivor.py index c25f376..7b8f9af 100644 --- a/scenarios/pension_survivor.py +++ b/scenarios/pension_survivor.py @@ -8,7 +8,6 @@ from .common import ( CLAIM_RESULT_FORMAT, PURPOSES, - SD_JWT_VC_FORMAT, StepHttpResult, auth_headers, credential_attempt, @@ -265,7 +264,6 @@ def authority_requests( purpose, subject, SURVIVOR_CLAIMS, - SD_JWT_VC_FORMAT, ) ] if step_id == "cause-of-death-denial": diff --git a/scripts/build-relay-runtime.sh b/scripts/build-relay-runtime.sh new file mode 100755 index 0000000..7915c3d --- /dev/null +++ b/scripts/build-relay-runtime.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env sh +set -eu + +root=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd) + +set -a +# shellcheck disable=SC1091 +. "$root/versions.env" +set +a + +source_ref=${REGISTRY_STACK_SOURCE_REF:?missing REGISTRY_STACK_SOURCE_REF} +source_commit=${REGISTRY_STACK_SOURCE_COMMIT:?missing REGISTRY_STACK_SOURCE_COMMIT} +relay_features=${REGISTRY_RELAY_FEATURES:?missing REGISTRY_RELAY_FEATURES} +runtime_image=${SOLMARA_RELAY_RUNTIME_IMAGE:?missing SOLMARA_RELAY_RUNTIME_IMAGE} +release_image=${REGISTRY_RELAY_IMAGE:?missing REGISTRY_RELAY_IMAGE} +platform=${REGISTRY_STACK_PLATFORM:-linux/amd64} + +case "$source_ref" in + v[0-9]*.[0-9]*.[0-9]*) ;; + *) + echo "REGISTRY_STACK_SOURCE_REF must be a stable vMAJOR.MINOR.PATCH tag" >&2 + exit 1 + ;; +esac +case "$source_commit" in + *[!0-9a-f]*) + echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 + exit 1 + ;; +esac +if [ "${#source_commit}" -ne 40 ]; then + echo "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" >&2 + exit 1 +fi +case "$relay_features" in + "" | *[!a-z0-9,-]*) + echo "REGISTRY_RELAY_FEATURES must be a comma-separated lowercase feature list" >&2 + exit 1 + ;; +esac + +image_revision=$( + docker image inspect \ + --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' \ + "$runtime_image" 2>/dev/null || true +) +image_features=$( + docker image inspect \ + --format '{{ index .Config.Labels "org.registrystack.solmara.relay-features" }}' \ + "$runtime_image" 2>/dev/null || true +) +image_base=$( + docker image inspect \ + --format '{{ index .Config.Labels "org.opencontainers.image.base.name" }}' \ + "$runtime_image" 2>/dev/null || true +) +if [ "$image_revision" = "$source_commit" ] \ + && [ "$image_features" = "$relay_features" ] \ + && [ "$image_base" = "$release_image" ]; then + echo "Solmara Relay runtime already matches $source_ref ($relay_features)" + exit 0 +fi + +if [ -n "${REGISTRY_STACK_SOURCE_DIR:-}" ]; then + source_dir=$REGISTRY_STACK_SOURCE_DIR +else + source_dir="$root/.cache/registry-stack/$source_commit" + if [ ! -d "$source_dir/.git" ]; then + mkdir -p "$(dirname -- "$source_dir")" + git clone --filter=blob:none --depth 1 --branch "$source_ref" \ + https://github.com/registrystack/registry-stack.git "$source_dir" + fi +fi + +actual_commit=$(git -C "$source_dir" rev-parse HEAD) +if [ "$actual_commit" != "$source_commit" ]; then + echo "Registry Stack source mismatch: expected $source_commit, found $actual_commit" >&2 + exit 1 +fi +if [ -n "$(git -C "$source_dir" status --porcelain)" ]; then + echo "Registry Stack source checkout must be clean: $source_dir" >&2 + exit 1 +fi + +docker buildx build \ + --load \ + --platform "$platform" \ + --build-arg "REGISTRY_RELAY_IMAGE=$release_image" \ + --build-arg "REGISTRY_STACK_SOURCE_COMMIT=$source_commit" \ + --build-arg "REGISTRY_RELAY_FEATURES=$relay_features" \ + --tag "$runtime_image" \ + --file "$root/docker/relay-runtime/Dockerfile" \ + "$source_dir" diff --git a/scripts/check-fiction.sh b/scripts/check-fiction.sh index 1568f1c..da077eb 100755 --- a/scripts/check-fiction.sh +++ b/scripts/check-fiction.sh @@ -21,6 +21,7 @@ for term in "${forbidden[@]}"; do --exclude-dir=.git \ --exclude-dir=node_modules \ --exclude-dir=.venv \ + --exclude-dir=.cache \ --exclude-dir=.svelte-kit \ --exclude-dir=build \ --exclude-dir=output \ diff --git a/scripts/check-nid-aliases.py b/scripts/check-nid-aliases.py index 3146e50..34dce05 100755 --- a/scripts/check-nid-aliases.py +++ b/scripts/check-nid-aliases.py @@ -9,6 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] EXCLUDED_DIRS = { + ".cache", ".git", ".venv", ".svelte-kit", diff --git a/scripts/check-release-pins.py b/scripts/check-release-pins.py index 96dbc94..884a87c 100755 --- a/scripts/check-release-pins.py +++ b/scripts/check-release-pins.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Verify Registry Stack image pins match a published release tag.""" +"""Verify Registry Stack release inputs match a published release tag.""" from __future__ import annotations @@ -20,6 +20,7 @@ r"^v(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)\.(?:0|[1-9][0-9]*)" r"(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$" ) +COMMIT_RE = re.compile(r"^[0-9a-f]{40}$") def main(argv: list[str]) -> int: @@ -38,6 +39,34 @@ def main(argv: list[str]) -> int: versions = read_versions(ROOT / "versions.env") failures: list[str] = [] + release_version = tag.removeprefix("v") + + registryctl_version = versions.get("REGISTRYCTL_VERSION") + if registryctl_version != release_version: + failures.append( + "REGISTRYCTL_VERSION from versions.env " + f"is {registryctl_version or 'missing'}, expected {release_version}" + ) + + source_ref = versions.get("REGISTRY_STACK_SOURCE_REF") + if source_ref != tag: + failures.append( + "REGISTRY_STACK_SOURCE_REF from versions.env " + f"is {source_ref or 'missing'}, expected {tag}" + ) + source_commit = versions.get("REGISTRY_STACK_SOURCE_COMMIT") + if not source_commit or not COMMIT_RE.fullmatch(source_commit): + failures.append( + "REGISTRY_STACK_SOURCE_COMMIT must be exactly 40 lowercase hex characters" + ) + relay_features = set( + filter(None, versions.get("REGISTRY_RELAY_FEATURES", "").split(",")) + ) + if "attribute-release" not in relay_features: + failures.append( + "REGISTRY_RELAY_FEATURES must include attribute-release " + "for the Solmara eSignet profile" + ) for key in IMAGE_KEYS: pinned = versions.get(key) @@ -74,7 +103,7 @@ def main(argv: list[str]) -> int: print(f"check-release-pins: {failure}", file=sys.stderr) return 1 - print(f"check-release-pins: Registry Stack images match {tag}") + print(f"check-release-pins: Registry Stack release inputs match {tag}") return 0 diff --git a/scripts/contract-generation-proof.py b/scripts/contract-generation-proof.py index bd19b0d..6021850 100755 --- a/scripts/contract-generation-proof.py +++ b/scripts/contract-generation-proof.py @@ -328,7 +328,12 @@ def successful_evaluation(payload: Any, subject: str) -> bool: ) -def relay_activity_counts(compose: Sequence[str], environment: Mapping[str, str]) -> tuple[int, int]: +def relay_activity_counts( + compose: Sequence[str], environment: Mapping[str, str] +) -> tuple[int, int]: + state_epoch = environment.get("REGISTRY_RELAY_STATE_EPOCH", "") + if re.fullmatch(r"[a-z0-9_]+", state_epoch) is None: + raise ProofFailure("REGISTRY_RELAY_STATE_EPOCH is missing or invalid") sql = ( "SELECT (SELECT count(*) FROM relay_state_private.consultation_completion_intent)," "(SELECT count(*) FROM relay_state_private.dispatch_permit WHERE dispatched_at IS NOT NULL);" @@ -344,7 +349,7 @@ def relay_activity_counts(compose: Sequence[str], environment: Mapping[str, str] "--username", user, "--dbname", - "solmara_relay_sro_consultation", + f"solmara_relay_sro_consultation_{state_epoch}", "--tuples-only", "--no-align", "--command", diff --git a/scripts/init-notary-postgresql.sh b/scripts/init-notary-postgresql.sh index 57c487f..60010bc 100755 --- a/scripts/init-notary-postgresql.sh +++ b/scripts/init-notary-postgresql.sh @@ -7,12 +7,20 @@ set -eu # databases. Passwords arrive through the deployment secret store and are # never printed. +relay_state_epoch=${REGISTRY_RELAY_STATE_EPOCH:-v013} +case "$relay_state_epoch" in + "" | *[!a-z0-9_]*) + echo "REGISTRY_RELAY_STATE_EPOCH must contain only lowercase letters, digits, and underscores" >&2 + exit 1 + ;; +esac + provision_relay() { key=$1 runtime_password=$2 maintenance_password=$3 reader_password=$4 - database="solmara_relay_${key}_consultation" + database="solmara_relay_${key}_consultation_${relay_state_epoch}" owner="${database}_owner" runtime="${database}_runtime" maintenance="${database}_keyring_maintenance" diff --git a/scripts/registry-projects.sh b/scripts/registry-projects.sh index 80e3a88..01f7b92 100755 --- a/scripts/registry-projects.sh +++ b/scripts/registry-projects.sh @@ -47,6 +47,25 @@ build_projects() { done } +check_projects() { + detail=$1 + for project in $projects; do + for environment in local hosted; do + echo "registryctl check: $project ($environment)" + if [ "$detail" = "explain" ]; then + "$REGISTRYCTL" check \ + --project-dir "$ROOT/projects/$project" \ + --environment "$environment" \ + --explain + else + "$REGISTRYCTL" check \ + --project-dir "$ROOT/projects/$project" \ + --environment "$environment" + fi + done + done +} + stage_runtime() { destination=$1 for environment in local hosted; do @@ -71,14 +90,10 @@ case "$action" in done ;; check) - for project in $projects; do - for environment in local hosted; do - echo "registryctl check: $project ($environment)" - "$REGISTRYCTL" check \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" - done - done + check_projects concise + ;; + review) + check_projects explain ;; build) environment=${2:-} @@ -107,7 +122,7 @@ case "$action" in diff -ruN "$ROOT/runtime/registry-projects" "$temporary/registry-projects" ;; *) - echo "usage: $0 |sync-runtime|check-runtime>" >&2 + echo "usage: $0 |sync-runtime|check-runtime>" >&2 exit 2 ;; esac diff --git a/scripts/review.sh b/scripts/review.sh index 981710f..dd00d8c 100755 --- a/scripts/review.sh +++ b/scripts/review.sh @@ -14,7 +14,7 @@ fi uv run --project "$root" "$root/scripts/publish-metadata.py" --check uv run --project "$root" "$root/scripts/metadata-lint.py" -if grep -RIn --exclude-dir=.git --exclude-dir=node_modules --exclude-dir=.venv --exclude-dir=ssl \ +if grep -RIn --exclude-dir=.git --exclude-dir=.cache --exclude-dir=node_modules --exclude-dir=.venv --exclude-dir=ssl \ -E "(BEGIN (RSA|OPENSSH|EC|PRIVATE) KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9_]{20,})" "$root"; then echo "Potential secret material found in repository files." >&2 exit 1 diff --git a/scripts/smoke-nia-attribute-release.py b/scripts/smoke-nia-attribute-release.py new file mode 100755 index 0000000..cbc2cd6 --- /dev/null +++ b/scripts/smoke-nia-attribute-release.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Prove the NIA eSignet attribute-release profile against the live Relay.""" + +from __future__ import annotations + +import json +import os +import sys +import urllib.error +import urllib.request +from typing import Any + + +PURPOSE = "https://id.registrystack.org/solmara/purpose/esignet-identity-verification" +PROFILE_PATH = ( + "/v1/attribute-releases/solmara-nia-userinfo/versions/v1/resolve" +) +SUBJECT = "2300018263" + + +def main() -> int: + token = os.environ.get("NIA_ESIGNET_RELAY_TOKEN", "") + if not token: + print( + "smoke-nia-attribute-release: missing NIA_ESIGNET_RELAY_TOKEN", + file=sys.stderr, + ) + return 1 + + base_url = os.environ.get("SOLMARA_NIA_RELAY_URL", "http://127.0.0.1:4312") + request = urllib.request.Request( + f"{base_url.rstrip('/')}{PROFILE_PATH}", + data=json.dumps( + {"subject": {"id_type": "national_id", "value": SUBJECT}} + ).encode("utf-8"), + headers={ + "Accept": "application/json", + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Data-Purpose": PURPOSE, + }, + method="POST", + ) + + try: + with urllib.request.urlopen(request, timeout=8.0) as response: + status = response.status + body = parse_json(response.read()) + except urllib.error.HTTPError as error: + status = error.code + body = parse_json(error.read()) + except Exception as error: + print( + "smoke-nia-attribute-release: request failed " + f"({error.__class__.__name__})", + file=sys.stderr, + ) + return 1 + + failure = validate_response(status, body) + if failure: + print(f"smoke-nia-attribute-release: {failure}", file=sys.stderr) + return 1 + + print( + "smoke-nia-attribute-release: governed eSignet identity resolution passed" + ) + return 0 + + +def parse_json(raw: bytes) -> Any: + try: + return json.loads(raw.decode("utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError): + return {} + + +def validate_response(status: int, body: Any) -> str | None: + if status != 200: + code = body.get("code") if isinstance(body, dict) else None + return f"expected HTTP 200, got {status}" + (f" {code}" if code else "") + if not isinstance(body, dict): + return "response was not a JSON object" + if body.get("profile_id") != "solmara-nia-userinfo": + return "response profile_id did not match" + if body.get("profile_version") != "v1": + return "response profile_version did not match" + claims = body.get("claims") + if not isinstance(claims, dict): + return "response omitted the minimized claims object" + if claims.get("individual_id") != SUBJECT or claims.get("name") != "Elena Dela Cruz": + return "response claims did not match the synthetic eSignet subject" + if "source" in body: + return "response disclosed source metadata" + return None + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/smoke.sh b/scripts/smoke.sh index ba4201a..79a5f69 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -11,6 +11,19 @@ ran=1 if [ "${SOLMARA_SMOKE_LIVE:-1}" != "0" ]; then ran=1 "$root/scripts/smoke-relay-sources.py" + compose=( + docker compose + --env-file "$root/versions.env" + --env-file "$root/.env" + -f "$root/compose.yaml" + ) + nia_esignet_relay_token=$( + "${compose[@]}" exec -T nia-workload-agent \ + cat /run/esignet-secrets/solmara-esignet-relay-token + ) + NIA_ESIGNET_RELAY_TOKEN="$nia_esignet_relay_token" \ + "$root/scripts/smoke-nia-attribute-release.py" + unset nia_esignet_relay_token # The signing smokes need cryptography from the locked project environment. uv run --locked --project "$root" "$root/scripts/smoke-live.py" uv run --locked --project "$root" "$root/scripts/notary_state_restart.py" diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index cdb29ab..a34a573 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -557,6 +557,26 @@ def test_registry_projects_are_explicit_and_use_the_pinned_registryctl( ] self.assertEqual(commands, expected) + log.write_text("", encoding="utf-8") + review = subprocess.run( + [str(ROOT / "scripts" / "registry-projects.sh"), "review"], + cwd=ROOT, + env={ + **os.environ, + "REGISTRYCTL_BIN": str(registryctl), + "REGISTRYCTL_LOG": str(log), + }, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + self.assertEqual(review.returncode, 0, review.stderr) + review_commands = log.read_text(encoding="utf-8").splitlines() + self.assertEqual( + review_commands, + [f"{command} --explain" for command in expected], + ) + registryctl.write_text( "#!/bin/sh\necho 'registryctl 0.8.3'\n", encoding="utf-8", @@ -622,6 +642,130 @@ def collect(value) -> None: self.assertEqual(consumed - produced, set()) self.assertEqual(consumed - declared, set()) + def test_relay_consultation_state_uses_the_v013_epoch(self) -> None: + versions = dict( + line.split("=", 1) + for line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines() + if line and not line.startswith("#") and "=" in line + ) + self.assertEqual(versions["REGISTRY_RELAY_STATE_EPOCH"], "v013") + + compose_names = ( + "compose.yaml", + "compose.coolify.interior.yaml", + "compose.coolify.social-development.yaml", + "compose.coolify.labour-pensions.yaml", + "compose.coolify.agriculture.yaml", + ) + epoch_reference = "${REGISTRY_RELAY_STATE_EPOCH:-v013}" + for compose_name in compose_names: + with self.subTest(compose=compose_name): + compose_path = ROOT / compose_name + raw = compose_path.read_text(encoding="utf-8") + compose = yaml.safe_load(raw) + services = compose["services"] + self.assertEqual( + services["postgres"]["environment"][ + "REGISTRY_RELAY_STATE_EPOCH" + ], + epoch_reference, + ) + self.assertEqual( + services["postgres"]["healthcheck"]["test"], + [ + "CMD-SHELL", + "pg_isready -h 127.0.0.1 -U " + "$${POSTGRES_USER} -d $${POSTGRES_DB}", + ], + ) + for authority in ("cra", "nia", "sro", "programme", "sipf", "nagdi"): + legacy = f"solmara_relay_{authority}_consultation" + self.assertNotIn(f"{legacy}_runtime", raw) + self.assertNotIn(f"{legacy}_owner", raw) + self.assertNotIn(f"/{legacy}?sslmode=require", raw) + + state_urls = [ + environment["REGISTRY_RELAY_CONSULTATION_DATABASE_URL"] + for service in services.values() + if ( + environment := service.get("environment") + ) + and "REGISTRY_RELAY_CONSULTATION_DATABASE_URL" in environment + ] + self.assertTrue(state_urls) + self.assertTrue( + all(f"_consultation_{epoch_reference}" in url for url in state_urls) + ) + + bootstrap_commands = [ + service["command"] + for name, service in services.items() + if name.endswith("-relay-state-bootstrap") + ] + self.assertTrue(bootstrap_commands) + for command in bootstrap_commands: + owner = command[command.index("--owner-role") + 1] + self.assertIn(f"_consultation_{epoch_reference}_owner", owner) + + provisioner = (ROOT / "scripts" / "init-notary-postgresql.sh").read_text( + encoding="utf-8" + ) + self.assertIn( + 'database="solmara_relay_${key}_consultation_${relay_state_epoch}"', + provisioner, + ) + self.assertIn( + "REGISTRY_RELAY_STATE_EPOCH must contain only lowercase letters", + provisioner, + ) + + def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: + versions = dict( + line.split("=", 1) + for line in (ROOT / "versions.env").read_text(encoding="utf-8").splitlines() + if line and not line.startswith("#") and "=" in line + ) + self.assertEqual(versions["REGISTRY_STACK_SOURCE_REF"], "v0.13.0") + self.assertRegex(versions["REGISTRY_STACK_SOURCE_COMMIT"], r"^[0-9a-f]{40}$") + self.assertEqual(versions["REGISTRY_RELAY_FEATURES"], "attribute-release") + + dockerfile = ( + ROOT / "docker" / "relay-runtime" / "Dockerfile" + ).read_text(encoding="utf-8") + self.assertIn("FROM ${REGISTRY_RELAY_IMAGE}", dockerfile) + self.assertIn( + 'LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}"', + dockerfile, + ) + self.assertIn("--features \"${REGISTRY_RELAY_FEATURES}\"", dockerfile) + self.assertNotIn("--all-features", dockerfile) + + justfile = (ROOT / "justfile").read_text(encoding="utf-8") + self.assertEqual(justfile.count("scripts/build-relay-runtime.sh"), 2) + self.assertEqual( + justfile.count( + 'REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_IMAGE"' + ), + 2, + ) + + release_workflow = ( + ROOT / ".github" / "workflows" / "release-candidate.yml" + ).read_text(encoding="utf-8") + prepare_build = release_workflow.index( + "- name: Prepare feature-enabled Relay runtime build" + ) + self.assertLess( + release_workflow.index("- name: Visitor center e2e"), + prepare_build, + ) + self.assertLess( + prepare_build, + release_workflow.index( + "- name: Build and push feature-enabled Relay runtime" + ), + ) + def test_coolify_authority_state_is_postgresql_isolated(self) -> None: authority_groups = { "compose.coolify.interior.yaml": ( diff --git a/scripts/test_release_pins.py b/scripts/test_release_pins.py index 8ccb42b..d9a3ec0 100644 --- a/scripts/test_release_pins.py +++ b/scripts/test_release_pins.py @@ -42,7 +42,12 @@ def setUp(self) -> None: self.root = Path(self.directory.name) self.module.ROOT = self.root (self.root / "versions.env").write_text( - f"REGISTRY_RELAY_IMAGE={RELAY}\nREGISTRY_NOTARY_IMAGE={NOTARY}\n", + f"REGISTRY_RELAY_IMAGE={RELAY}\n" + f"REGISTRY_NOTARY_IMAGE={NOTARY}\n" + "REGISTRYCTL_VERSION=1.0.0\n" + "REGISTRY_STACK_SOURCE_REF=v1.0.0\n" + f"REGISTRY_STACK_SOURCE_COMMIT={'a' * 40}\n" + "REGISTRY_RELAY_FEATURES=attribute-release\n", encoding="utf-8", ) @@ -99,6 +104,22 @@ def test_candidate_test_passes_with_workflow_image_environment(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) def test_candidate_prerelease_tag_is_accepted(self) -> None: + versions = (self.root / "versions.env").read_text(encoding="utf-8") + (self.root / "versions.env").write_text( + versions.replace( + "REGISTRYCTL_VERSION=1.0.0", + "REGISTRYCTL_VERSION=1.0.0-rc.1", + ), + encoding="utf-8", + ) + versions = (self.root / "versions.env").read_text(encoding="utf-8") + (self.root / "versions.env").write_text( + versions.replace( + "REGISTRY_STACK_SOURCE_REF=v1.0.0", + "REGISTRY_STACK_SOURCE_REF=v1.0.0-rc.1", + ), + encoding="utf-8", + ) with mock.patch.object( self.module, "inspect_tag_digest", @@ -109,6 +130,57 @@ def test_candidate_prerelease_tag_is_accepted(self) -> None: 0, ) + def test_registryctl_version_must_match_release_tag(self) -> None: + versions = (self.root / "versions.env").read_text(encoding="utf-8") + (self.root / "versions.env").write_text( + versions.replace( + "REGISTRYCTL_VERSION=1.0.0", + "REGISTRYCTL_VERSION=0.13.0", + ), + encoding="utf-8", + ) + stderr = io.StringIO() + with ( + mock.patch.object(self.module, "inspect_tag_digest") as inspect, + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) + + inspect.assert_not_called() + self.assertIn( + "REGISTRYCTL_VERSION from versions.env is 0.13.0, expected 1.0.0", + stderr.getvalue(), + ) + + def test_relay_source_ref_and_feature_must_match_the_release(self) -> None: + versions = (self.root / "versions.env").read_text(encoding="utf-8") + (self.root / "versions.env").write_text( + versions.replace( + "REGISTRY_STACK_SOURCE_REF=v1.0.0", + "REGISTRY_STACK_SOURCE_REF=v0.13.0", + ).replace( + "REGISTRY_RELAY_FEATURES=attribute-release", + "REGISTRY_RELAY_FEATURES=ogcapi-features", + ), + encoding="utf-8", + ) + stderr = io.StringIO() + with ( + mock.patch.object(self.module, "inspect_tag_digest") as inspect, + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) + + inspect.assert_not_called() + self.assertIn( + "REGISTRY_STACK_SOURCE_REF from versions.env is v0.13.0, expected v1.0.0", + stderr.getvalue(), + ) + self.assertIn( + "REGISTRY_RELAY_FEATURES must include attribute-release", + stderr.getvalue(), + ) + def test_mismatched_environment_override_fails_before_registry_lookup(self) -> None: stderr = io.StringIO() with ( diff --git a/scripts/test_smoke_nia_attribute_release.py b/scripts/test_smoke_nia_attribute_release.py new file mode 100644 index 0000000..74804a8 --- /dev/null +++ b/scripts/test_smoke_nia_attribute_release.py @@ -0,0 +1,109 @@ +from __future__ import annotations + +import importlib.util +import io +import json +import os +import unittest +from contextlib import redirect_stderr +from pathlib import Path +from unittest import mock + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "smoke_nia_attribute_release", + ROOT / "scripts" / "smoke-nia-attribute-release.py", +) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class FakeResponse: + def __init__(self, body: dict[str, object]) -> None: + self.status = 200 + self.body = body + + def __enter__(self) -> FakeResponse: + return self + + def __exit__(self, *_args: object) -> None: + return None + + def read(self) -> bytes: + return json.dumps(self.body).encode("utf-8") + + +class NiaAttributeReleaseSmokeTests(unittest.TestCase): + def test_live_request_uses_the_minimized_profile_contract(self) -> None: + response = FakeResponse( + { + "profile_id": "solmara-nia-userinfo", + "profile_version": "v1", + "claims": { + "individual_id": MODULE.SUBJECT, + "name": "Elena Dela Cruz", + }, + } + ) + with ( + mock.patch.dict( + os.environ, + {"NIA_ESIGNET_RELAY_TOKEN": "runtime-token"}, + clear=True, + ), + mock.patch.object( + MODULE.urllib.request, + "urlopen", + return_value=response, + ) as urlopen, + ): + self.assertEqual(MODULE.main(), 0) + + request = urlopen.call_args.args[0] + self.assertEqual(request.method, "POST") + self.assertEqual(request.get_header("Authorization"), "Bearer runtime-token") + self.assertEqual(request.get_header("Data-purpose"), MODULE.PURPOSE) + self.assertEqual( + json.loads(request.data), + { + "subject": { + "id_type": "national_id", + "value": MODULE.SUBJECT, + } + }, + ) + + def test_source_metadata_is_rejected(self) -> None: + stderr = io.StringIO() + with ( + mock.patch.dict( + os.environ, + {"NIA_ESIGNET_RELAY_TOKEN": "runtime-token"}, + clear=True, + ), + mock.patch.object( + MODULE.urllib.request, + "urlopen", + return_value=FakeResponse( + { + "profile_id": "solmara-nia-userinfo", + "profile_version": "v1", + "claims": { + "individual_id": MODULE.SUBJECT, + "name": "Elena Dela Cruz", + }, + "source": {"dataset": "population"}, + } + ), + ), + redirect_stderr(stderr), + ): + self.assertEqual(MODULE.main(), 1) + + self.assertIn("response disclosed source metadata", stderr.getvalue()) + + +if __name__ == "__main__": + unittest.main() diff --git a/versions.env b/versions.env index 6a87f13..b7e6acd 100644 --- a/versions.env +++ b/versions.env @@ -1,6 +1,11 @@ -REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:81d0b97e60560159a0f77ee10008a859f4e5dc15fc70fcb2c2fb50f8f190617f -REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:a1206ca0e6059c3bd88b84060a9be28cf5399abae6c94a9bf1d545cdaeefc639 -REGISTRYCTL_VERSION=0.10.0 +REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:00681003d06667cde377e15640a3825eec97aca26f486cde2e9f5013ef56819e +REGISTRY_NOTARY_IMAGE=ghcr.io/registrystack/registry-notary@sha256:6848ad62789bd6e5a1af4262f85e9a0110bea9ed8e3f9297adab19cb62f8f297 +REGISTRYCTL_VERSION=0.13.0 +REGISTRY_STACK_SOURCE_REF=v0.13.0 +REGISTRY_STACK_SOURCE_COMMIT=985022e58c627967480b8ad304fbbd250ec7193d +REGISTRY_RELAY_FEATURES=attribute-release +SOLMARA_RELAY_RUNTIME_IMAGE=solmara-lab-registry-relay:v0.13.0-attribute-release +REGISTRY_RELAY_STATE_EPOCH=v013 POSTGRES_IMAGE=postgres:16-alpine STATIC_METADATA_IMAGE=python:3.12-alpine VOLUME_INIT_IMAGE=busybox:1.36 From 86696d258d06508490aafeeeb318211f8a9f9c02 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 22:09:20 +0700 Subject: [PATCH 02/14] Use v0.13 config workflow defaults Signed-off-by: Jeremi Joslin --- README.md | 13 ++- docs/changelog.md | 5 +- projects/cra-civil/environments/hosted.yaml | 2 - .../mosd-programme/environments/hosted.yaml | 3 - .../environments/hosted.yaml | 3 - .../nia-population/environments/hosted.yaml | 2 - .../sipf-pensions/environments/hosted.yaml | 2 - projects/sro-social/environments/hosted.yaml | 3 - .../hosted/cra-civil/notary/notary.yaml | 2 - .../hosted/mosd-programme/notary/notary.yaml | 2 - .../nagdi-agriculture/notary/notary.yaml | 2 - .../hosted/nia-population/notary/notary.yaml | 2 - .../hosted/sipf-pensions/notary/notary.yaml | 2 - .../hosted/sro-social/notary/notary.yaml | 2 - scripts/registry-projects.sh | 26 ++++- scripts/registryctl-build-output.py | 110 ++++++++++++++++++ scripts/test_quality_scripts.py | 94 ++++++++++----- scripts/test_registryctl_build_output.py | 101 ++++++++++++++++ 18 files changed, 308 insertions(+), 68 deletions(-) create mode 100755 scripts/registryctl-build-output.py create mode 100644 scripts/test_registryctl_build_output.py diff --git a/README.md b/README.md index c70454f..9024e20 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/docs/changelog.md b/docs/changelog.md index a3bd2a4..c7b04eb 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -19,7 +19,10 @@ sharing the retained result state plane. `registry-projects-review` exposes Registryctl's complete redacted acquisition and disclosure explanation when a reviewer needs more detail than the concise default report. Release-pin review now also rejects a Registryctl version that does not match the selected -Registry Stack tag. +Registry Stack tag. Runtime staging consumes Registryctl's versioned JSON build +report and validates its project-owned output root instead of constructing a +private compiler path. Hosted Notary profiles now inherit the product's 128 MiB +CEL worker default; the 1 GiB override remains limited to emulated local runs. The NIA eSignet profile remains explicit beta functionality. Solmara now builds its Relay runtime from the exact v0.13.0 source commit with only the diff --git a/projects/cra-civil/environments/hosted.yaml b/projects/cra-civil/environments/hosted.yaml index 1a772c2..f5fdebd 100644 --- a/projects/cra-civil/environments/hosted.yaml +++ b/projects/cra-civil/environments/hosted.yaml @@ -38,8 +38,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab relay: { service: cra-civil-relay } diff --git a/projects/mosd-programme/environments/hosted.yaml b/projects/mosd-programme/environments/hosted.yaml index 5271693..26fb468 100644 --- a/projects/mosd-programme/environments/hosted.yaml +++ b/projects/mosd-programme/environments/hosted.yaml @@ -36,9 +36,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 - deployment: profile: hosted_lab relay: { service: programme-mis-relay } diff --git a/projects/nagdi-agriculture/environments/hosted.yaml b/projects/nagdi-agriculture/environments/hosted.yaml index e523467..e7a65f1 100644 --- a/projects/nagdi-agriculture/environments/hosted.yaml +++ b/projects/nagdi-agriculture/environments/hosted.yaml @@ -64,9 +64,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 - deployment: profile: hosted_lab relay: { service: nagdi-agriculture-relay } diff --git a/projects/nia-population/environments/hosted.yaml b/projects/nia-population/environments/hosted.yaml index ae0d74e..d542939 100644 --- a/projects/nia-population/environments/hosted.yaml +++ b/projects/nia-population/environments/hosted.yaml @@ -45,8 +45,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab relay: { service: nia-population-relay } diff --git a/projects/sipf-pensions/environments/hosted.yaml b/projects/sipf-pensions/environments/hosted.yaml index 85bdebc..9977b87 100644 --- a/projects/sipf-pensions/environments/hosted.yaml +++ b/projects/sipf-pensions/environments/hosted.yaml @@ -49,8 +49,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab relay: { service: sipf-pensions-relay } diff --git a/projects/sro-social/environments/hosted.yaml b/projects/sro-social/environments/hosted.yaml index 809de9a..b2888ea 100644 --- a/projects/sro-social/environments/hosted.yaml +++ b/projects/sro-social/environments/hosted.yaml @@ -36,9 +36,6 @@ relay_state: notary_state: postgresql: root_certificate_path: /etc/solmara/postgres/root.crt -notary_cel: - worker_memory_bytes: 1073741824 - deployment: profile: hosted_lab relay: { service: sro-social-relay } diff --git a/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml b/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml index aafa99b..bee0c29 100644 --- a/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml +++ b/runtime/registry-projects/hosted/cra-civil/notary/notary.yaml @@ -21,8 +21,6 @@ auth: id: pension-review-desk scopes: - evidence:cra:pension:read -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml b/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml index cd4aa2f..6dd83e0 100644 --- a/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml +++ b/runtime/registry-projects/hosted/mosd-programme/notary/notary.yaml @@ -9,8 +9,6 @@ auth: id: child-benefit-federator scopes: - mosd_programme:evidence_verification -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml b/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml index 527ec4e..00b03fb 100644 --- a/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml +++ b/runtime/registry-projects/hosted/nagdi-agriculture/notary/notary.yaml @@ -9,8 +9,6 @@ auth: id: nagdi-service scopes: - nagdi_agriculture:evidence_verification -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml b/runtime/registry-projects/hosted/nia-population/notary/notary.yaml index baa86ef..8fa7c0a 100644 --- a/runtime/registry-projects/hosted/nia-population/notary/notary.yaml +++ b/runtime/registry-projects/hosted/nia-population/notary/notary.yaml @@ -15,8 +15,6 @@ auth: id: citizen-portal scopes: - evidence:nia:citizen:read -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml b/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml index 055000d..ea8504a 100644 --- a/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml +++ b/runtime/registry-projects/hosted/sipf-pensions/notary/notary.yaml @@ -10,8 +10,6 @@ auth: scopes: - evidence:sipf:pension-payment:read - evidence:sipf:survivor-benefit:read -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/runtime/registry-projects/hosted/sro-social/notary/notary.yaml b/runtime/registry-projects/hosted/sro-social/notary/notary.yaml index 2e32994..06d03f4 100644 --- a/runtime/registry-projects/hosted/sro-social/notary/notary.yaml +++ b/runtime/registry-projects/hosted/sro-social/notary/notary.yaml @@ -9,8 +9,6 @@ auth: id: child-benefit-federator scopes: - sro_social:evidence_verification -cel: - worker_memory_bytes: 1073741824 deployment: profile: hosted_lab evidence: diff --git a/scripts/registry-projects.sh b/scripts/registry-projects.sh index 01f7b92..9bb5303 100755 --- a/scripts/registry-projects.sh +++ b/scripts/registry-projects.sh @@ -37,13 +37,27 @@ sipf-pensions nagdi-agriculture " +build_project_output() { + project=$1 + environment=$2 + project_directory="$ROOT/projects/$project" + echo "registryctl build: $project ($environment)" >&2 + build_report=$( + "$REGISTRYCTL" build \ + --project-dir "$project_directory" \ + --environment "$environment" \ + --format json + ) + printf '%s\n' "$build_report" | + python3 "$ROOT/scripts/registryctl-build-output.py" \ + --project-dir "$project_directory" \ + --environment "$environment" +} + build_projects() { environment=$1 for project in $projects; do - echo "registryctl build: $project ($environment)" - "$REGISTRYCTL" build \ - --project-dir "$ROOT/projects/$project" \ - --environment "$environment" + build_project_output "$project" "$environment" done } @@ -69,9 +83,9 @@ check_projects() { stage_runtime() { destination=$1 for environment in local hosted; do - build_projects "$environment" for project in $projects; do - source="$ROOT/projects/$project/.registry-stack/build/$environment/private" + build_root=$(build_project_output "$project" "$environment") + source="$build_root/private" target="$destination/$environment/$project" mkdir -p "$target/relay" "$target/notary" cp -R "$source/relay/config/." "$target/relay/" diff --git a/scripts/registryctl-build-output.py b/scripts/registryctl-build-output.py new file mode 100755 index 0000000..1393ced --- /dev/null +++ b/scripts/registryctl-build-output.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python3 +"""Validate a registryctl build report and print its generated output root.""" + +from __future__ import annotations + +import argparse +import json +import sys +from pathlib import Path +from typing import Any + + +REPORT_SCHEMA = "registryctl.project_command.v1" +MAX_REPORT_BYTES = 8 * 1024 * 1024 + + +class BuildReportError(ValueError): + """A safe validation error for a registryctl build report.""" + + +def reject_duplicate_fields(pairs: list[tuple[str, Any]]) -> dict[str, Any]: + result: dict[str, Any] = {} + for key, value in pairs: + if key in result: + raise BuildReportError("the JSON report contains a duplicate field") + result[key] = value + return result + + +def parse_build_output( + raw: bytes, + *, + project_directory: Path, + environment: str, +) -> Path: + if not raw: + raise BuildReportError("registryctl returned an empty report") + if len(raw) > MAX_REPORT_BYTES: + raise BuildReportError("the JSON report exceeds the size limit") + try: + report = json.loads(raw, object_pairs_hook=reject_duplicate_fields) + except (json.JSONDecodeError, UnicodeDecodeError, RecursionError) as error: + raise BuildReportError("registryctl did not return strict UTF-8 JSON") from error + if not isinstance(report, dict): + raise BuildReportError("the JSON report is not an object") + if report.get("schema_version") != REPORT_SCHEMA: + raise BuildReportError("the JSON report has an unsupported schema") + if report.get("status") != "built": + raise BuildReportError("registryctl did not report a completed build") + if report.get("environment") != environment: + raise BuildReportError("the JSON report has the wrong environment binding") + if not isinstance(report.get("project"), str) or not report["project"]: + raise BuildReportError("the JSON report has no project identity") + + output_value = report.get("output") + if not isinstance(output_value, str) or not output_value: + raise BuildReportError("the JSON report has no output root") + output = Path(output_value) + if not output.is_absolute(): + raise BuildReportError("the JSON report output root is not absolute") + + try: + project_root = project_directory.resolve(strict=True) + output_root = output.resolve(strict=True) + output_root.relative_to(project_root) + except (FileNotFoundError, RuntimeError, ValueError) as error: + raise BuildReportError( + "the JSON report output root is not a real project-owned directory" + ) from error + if not output_root.is_dir(): + raise BuildReportError("the JSON report output root is not a directory") + + relay_config = output_root / "private" / "relay" / "config" + notary_config = output_root / "private" / "notary" / "config" / "notary.yaml" + if not relay_config.is_dir() or not notary_config.is_file(): + raise BuildReportError( + "the generated Relay or Notary configuration closure is incomplete" + ) + for required in (relay_config, notary_config): + try: + required.resolve(strict=True).relative_to(output_root) + except (FileNotFoundError, RuntimeError, ValueError) as error: + raise BuildReportError( + "a generated configuration path escapes the build output root" + ) from error + + return output_root + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--project-dir", required=True, type=Path) + parser.add_argument("--environment", required=True) + args = parser.parse_args(argv) + raw = sys.stdin.buffer.read(MAX_REPORT_BYTES + 1) + try: + output = parse_build_output( + raw, + project_directory=args.project_dir, + environment=args.environment, + ) + except BuildReportError as error: + print(f"registryctl build report invalid: {error}", file=sys.stderr) + return 1 + print(output) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index a34a573..03eec72 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -78,7 +78,7 @@ def test_performance_guide_references_every_k6_entrypoint(self) -> None: } self.assertEqual(referenced, entrypoints) - def test_authority_notary_cel_ceiling_is_explicit_and_generated(self) -> None: + def test_authority_notary_cel_ceiling_is_local_only_and_generated(self) -> None: projects = ( "cra-civil", "nia-population", @@ -87,34 +87,60 @@ def test_authority_notary_cel_ceiling_is_explicit_and_generated(self) -> None: "sipf-pensions", "nagdi-agriculture", ) - for environment in ("local", "hosted"): - for project in projects: - with self.subTest(environment=environment, project=project): - authored = yaml.safe_load( - ( - ROOT - / "projects" - / project - / "environments" - / f"{environment}.yaml" - ).read_text(encoding="utf-8") - ) - generated = yaml.safe_load( - ( - ROOT - / "runtime" - / "registry-projects" - / environment - / project - / "notary" - / "notary.yaml" - ).read_text(encoding="utf-8") - ) - self.assertEqual( - authored["notary_cel"], - {"worker_memory_bytes": 1_073_741_824}, - ) - self.assertEqual(generated["cel"], authored["notary_cel"]) + for project in projects: + with self.subTest(environment="local", project=project): + local_authored = yaml.safe_load( + ( + ROOT + / "projects" + / project + / "environments" + / "local.yaml" + ).read_text(encoding="utf-8") + ) + local_generated = yaml.safe_load( + ( + ROOT + / "runtime" + / "registry-projects" + / "local" + / project + / "notary" + / "notary.yaml" + ).read_text(encoding="utf-8") + ) + self.assertEqual( + local_authored["notary_cel"], + {"worker_memory_bytes": 1_073_741_824}, + ) + self.assertEqual( + local_generated["cel"], + local_authored["notary_cel"], + ) + + with self.subTest(environment="hosted", project=project): + hosted_authored = yaml.safe_load( + ( + ROOT + / "projects" + / project + / "environments" + / "hosted.yaml" + ).read_text(encoding="utf-8") + ) + hosted_generated = yaml.safe_load( + ( + ROOT + / "runtime" + / "registry-projects" + / "hosted" + / project + / "notary" + / "notary.yaml" + ).read_text(encoding="utf-8") + ) + self.assertNotIn("notary_cel", hosted_authored) + self.assertNotIn("cel", hosted_generated) def test_generated_secret_contract_uses_authority_owners(self) -> None: module = load_secret_generator() @@ -612,6 +638,16 @@ def test_registry_projects_are_explicit_and_use_the_pinned_registryctl( incompatible.stderr, ) + registry_projects = ( + ROOT / "scripts" / "registry-projects.sh" + ).read_text(encoding="utf-8") + self.assertIn("--format json", registry_projects) + self.assertIn("registryctl-build-output.py", registry_projects) + self.assertNotIn( + ".registry-stack/build/$environment/private", + registry_projects, + ) + def test_registry_project_secret_references_have_local_producers(self) -> None: module = load_secret_generator() produced = ( diff --git a/scripts/test_registryctl_build_output.py b/scripts/test_registryctl_build_output.py new file mode 100644 index 0000000..54a1834 --- /dev/null +++ b/scripts/test_registryctl_build_output.py @@ -0,0 +1,101 @@ +from __future__ import annotations + +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "registryctl_build_output", + ROOT / "scripts" / "registryctl-build-output.py", +) +assert SPEC and SPEC.loader +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class RegistryctlBuildOutputTests(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.project = Path(self.temporary.name) / "project" + self.project.mkdir() + (self.project / "registry-stack.yaml").write_text( + "version: 1\nregistry:\n id: example\n", + encoding="utf-8", + ) + self.output = self.project / ".registry-stack" / "build" / "local" + relay = self.output / "private" / "relay" / "config" + relay.mkdir(parents=True) + (relay / "relay.yaml").write_text("instance: {}\n", encoding="utf-8") + notary = self.output / "private" / "notary" / "config" + notary.mkdir(parents=True) + (notary / "notary.yaml").write_text("instance: {}\n", encoding="utf-8") + + def report(self, **overrides: object) -> bytes: + report: dict[str, object] = { + "schema_version": MODULE.REPORT_SCHEMA, + "status": "built", + "project": "example", + "environment": "local", + "fixtures": [], + "semantic_changes": [], + "baseline": "initial_without_baseline", + "output": str(self.output), + } + report.update(overrides) + return json.dumps(report).encode("utf-8") + + def test_accepts_the_versioned_project_owned_build_root(self) -> None: + self.assertEqual( + MODULE.parse_build_output( + self.report(), + project_directory=self.project, + environment="local", + ), + self.output.resolve(), + ) + + def test_rejects_the_wrong_environment_without_echoing_report_values(self) -> None: + with self.assertRaisesRegex( + MODULE.BuildReportError, + "wrong environment binding", + ) as rejected: + MODULE.parse_build_output( + self.report(environment="hosted", secret="must-not-echo"), + project_directory=self.project, + environment="local", + ) + self.assertNotIn("must-not-echo", str(rejected.exception)) + + def test_rejects_an_output_root_outside_the_project(self) -> None: + outside = Path(self.temporary.name) / "outside" + outside.mkdir() + with self.assertRaisesRegex( + MODULE.BuildReportError, + "not a real project-owned directory", + ): + MODULE.parse_build_output( + self.report(output=str(outside)), + project_directory=self.project, + environment="local", + ) + + def test_rejects_an_incomplete_product_closure(self) -> None: + (self.output / "private" / "notary" / "config" / "notary.yaml").unlink() + with self.assertRaisesRegex( + MODULE.BuildReportError, + "configuration closure is incomplete", + ): + MODULE.parse_build_output( + self.report(), + project_directory=self.project, + environment="local", + ) + + +if __name__ == "__main__": + unittest.main() From 572d772fce54321ae3dfe720c4e6a8b8dfb5d650 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 22:30:37 +0700 Subject: [PATCH 03/14] Keep normal startup on published Relay Signed-off-by: Jeremi Joslin --- README.md | 8 +++ justfile | 12 ++++- scripts/build-relay-runtime.sh | 2 +- scripts/check-release-pins.py | 58 +++++++++++++++++++++ scripts/review.sh | 5 +- scripts/test_quality_scripts.py | 30 +++++++++++ scripts/test_release_pins.py | 91 ++++++++++++++++++++++++++++++++- versions.env | 3 +- 8 files changed, 202 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9024e20..be336d0 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,10 @@ 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 @@ -91,6 +94,11 @@ just review # normal security and release-readiness checks just review-release # 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. diff --git a/justfile b/justfile index a57951b..51b841e 100644 --- a/justfile +++ b/justfile @@ -97,18 +97,26 @@ compose: # Start the local topology. up: - scripts/build-relay-runtime.sh @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build +# Build Registry Relay from the pinned source for explicit local development. +up-dev: + scripts/build-relay-runtime.sh + @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_DEV_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml up -d --build + # Stop the local topology without removing local volumes. down: @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml down # Start the local topology with eSignet-backed portal login. up-esignet: - scripts/build-relay-runtime.sh @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build +# Build Registry Relay from the pinned source for explicit local eSignet development. +up-esignet-dev: + scripts/build-relay-runtime.sh + @set -a; . ./versions.env; set +a; env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_DEV_IMAGE" COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml up -d --build + # Stop the local eSignet topology without removing local volumes. down-esignet: @env_args="--env-file versions.env"; if [ -f .env ]; then env_args="$env_args --env-file .env"; fi; COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-{{compose_project_name}}}" docker compose $env_args -f compose.yaml -f compose.esignet.yaml down diff --git a/scripts/build-relay-runtime.sh b/scripts/build-relay-runtime.sh index 7915c3d..c35d912 100755 --- a/scripts/build-relay-runtime.sh +++ b/scripts/build-relay-runtime.sh @@ -11,7 +11,7 @@ set +a source_ref=${REGISTRY_STACK_SOURCE_REF:?missing REGISTRY_STACK_SOURCE_REF} source_commit=${REGISTRY_STACK_SOURCE_COMMIT:?missing REGISTRY_STACK_SOURCE_COMMIT} relay_features=${REGISTRY_RELAY_FEATURES:?missing REGISTRY_RELAY_FEATURES} -runtime_image=${SOLMARA_RELAY_RUNTIME_IMAGE:?missing SOLMARA_RELAY_RUNTIME_IMAGE} +runtime_image=${SOLMARA_RELAY_RUNTIME_DEV_IMAGE:?missing SOLMARA_RELAY_RUNTIME_DEV_IMAGE} release_image=${REGISTRY_RELAY_IMAGE:?missing REGISTRY_RELAY_IMAGE} platform=${REGISTRY_STACK_PLATFORM:-linux/amd64} diff --git a/scripts/check-release-pins.py b/scripts/check-release-pins.py index 884a87c..087f9ea 100755 --- a/scripts/check-release-pins.py +++ b/scripts/check-release-pins.py @@ -14,6 +14,10 @@ "REGISTRY_RELAY_IMAGE": "ghcr.io/registrystack/registry-relay", "REGISTRY_NOTARY_IMAGE": "ghcr.io/registrystack/registry-notary", } +SOLMARA_RELAY_RUNTIME_REPOSITORY = ( + "ghcr.io/registrystack/solmara-lab-relay-runtime" +) +REGISTRY_STACK_REMOTE = "https://github.com/registrystack/registry-stack.git" PIN_RE = re.compile(r"^(?P[^@\s]+)@(?Psha256:[0-9a-f]{64})$") DIGEST_RE = re.compile(r"^Digest:\s+(sha256:[0-9a-f]{64})$", re.MULTILINE) TAG_RE = re.compile( @@ -67,6 +71,17 @@ def main(argv: list[str]) -> int: "REGISTRY_RELAY_FEATURES must include attribute-release " "for the Solmara eSignet profile" ) + relay_runtime = versions.get("SOLMARA_RELAY_RUNTIME_IMAGE") + relay_runtime_match = PIN_RE.match(relay_runtime or "") + if ( + not relay_runtime_match + or relay_runtime_match.group("image") + != SOLMARA_RELAY_RUNTIME_REPOSITORY + ): + failures.append( + "SOLMARA_RELAY_RUNTIME_IMAGE must pin " + f"{SOLMARA_RELAY_RUNTIME_REPOSITORY}@sha256:" + ) for key in IMAGE_KEYS: pinned = versions.get(key) @@ -79,6 +94,22 @@ def main(argv: list[str]) -> int: print(f"check-release-pins: {failure}", file=sys.stderr) return 1 + try: + tag_commit = resolve_tag_commit(tag) + except (RuntimeError, subprocess.CalledProcessError): + print( + f"check-release-pins: could not resolve Registry Stack tag {tag}", + file=sys.stderr, + ) + return 1 + if source_commit != tag_commit: + print( + "check-release-pins: REGISTRY_STACK_SOURCE_COMMIT from versions.env " + f"is {source_commit}, but {tag} resolves to {tag_commit}", + file=sys.stderr, + ) + return 1 + for key, image in IMAGE_KEYS.items(): pinned = versions.get(key) if not pinned: @@ -133,5 +164,32 @@ def inspect_tag_digest(ref: str) -> str: return match.group(1) +def resolve_tag_commit(tag: str) -> str: + direct_ref = f"refs/tags/{tag}" + peeled_ref = f"{direct_ref}^{{}}" + result = subprocess.run( + ["git", "ls-remote", REGISTRY_STACK_REMOTE, direct_ref, peeled_ref], + cwd=ROOT, + check=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + refs: dict[str, str] = {} + for raw_line in result.stdout.splitlines(): + fields = raw_line.split() + if len(fields) != 2 or fields[1] not in {direct_ref, peeled_ref}: + raise RuntimeError("Registry Stack tag lookup returned an invalid ref") + commit, ref = fields + if ref in refs or not COMMIT_RE.fullmatch(commit): + raise RuntimeError("Registry Stack tag lookup returned an invalid commit") + refs[ref] = commit + + commit = refs.get(peeled_ref) or refs.get(direct_ref) + if not commit: + raise RuntimeError("Registry Stack tag lookup returned no matching tag") + return commit + + if __name__ == "__main__": raise SystemExit(main(sys.argv)) diff --git a/scripts/review.sh b/scripts/review.sh index dd00d8c..e666dcd 100755 --- a/scripts/review.sh +++ b/scripts/review.sh @@ -14,8 +14,9 @@ fi uv run --project "$root" "$root/scripts/publish-metadata.py" --check uv run --project "$root" "$root/scripts/metadata-lint.py" -if grep -RIn --exclude-dir=.git --exclude-dir=.cache --exclude-dir=node_modules --exclude-dir=.venv --exclude-dir=ssl \ - -E "(BEGIN (RSA|OPENSSH|EC|PRIVATE) KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9_]{20,})" "$root"; then +if git -C "$root" grep -I -n \ + -E "(BEGIN (RSA|OPENSSH|EC|PRIVATE) KEY|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9_]{20,})" \ + -- .; then echo "Potential secret material found in repository files." >&2 exit 1 fi diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index 03eec72..bb25108 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -764,6 +764,17 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: self.assertEqual(versions["REGISTRY_STACK_SOURCE_REF"], "v0.13.0") self.assertRegex(versions["REGISTRY_STACK_SOURCE_COMMIT"], r"^[0-9a-f]{40}$") self.assertEqual(versions["REGISTRY_RELAY_FEATURES"], "attribute-release") + self.assertRegex( + versions["SOLMARA_RELAY_RUNTIME_IMAGE"], + ( + r"^ghcr\.io/registrystack/solmara-lab-relay-runtime" + r"@sha256:[0-9a-f]{64}$" + ), + ) + self.assertEqual( + versions["SOLMARA_RELAY_RUNTIME_DEV_IMAGE"], + "solmara-lab-registry-relay:v0.13.0-attribute-release", + ) dockerfile = ( ROOT / "docker" / "relay-runtime" / "Dockerfile" @@ -784,6 +795,25 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: ), 2, ) + self.assertEqual( + justfile.count( + 'REGISTRY_RELAY_IMAGE="$SOLMARA_RELAY_RUNTIME_DEV_IMAGE"' + ), + 2, + ) + normal_up = justfile.split("\nup:\n", 1)[1].split("\n\n", 1)[0] + normal_esignet = justfile.split("\nup-esignet:\n", 1)[1].split( + "\n\n", + 1, + )[0] + self.assertNotIn("build-relay-runtime.sh", normal_up) + self.assertNotIn("build-relay-runtime.sh", normal_esignet) + + review_script = (ROOT / "scripts" / "review.sh").read_text( + encoding="utf-8" + ) + self.assertIn('git -C "$root" grep -I -n', review_script) + self.assertNotIn("grep -RIn", review_script) release_workflow = ( ROOT / ".github" / "workflows" / "release-candidate.yml" diff --git a/scripts/test_release_pins.py b/scripts/test_release_pins.py index d9a3ec0..fb5ce2a 100644 --- a/scripts/test_release_pins.py +++ b/scripts/test_release_pins.py @@ -38,6 +38,14 @@ def setUp(self) -> None: os.environ.pop("REGISTRY_NOTARY_IMAGE", None) self.module = load_check_release_pins() + self.resolve_tag_implementation = self.module.resolve_tag_commit + self.resolve_tag = mock.patch.object( + self.module, + "resolve_tag_commit", + return_value="a" * 40, + ) + self.resolve_tag.start() + self.addCleanup(self.resolve_tag.stop) self.directory = tempfile.TemporaryDirectory() self.root = Path(self.directory.name) self.module.ROOT = self.root @@ -47,7 +55,10 @@ def setUp(self) -> None: "REGISTRYCTL_VERSION=1.0.0\n" "REGISTRY_STACK_SOURCE_REF=v1.0.0\n" f"REGISTRY_STACK_SOURCE_COMMIT={'a' * 40}\n" - "REGISTRY_RELAY_FEATURES=attribute-release\n", + "REGISTRY_RELAY_FEATURES=attribute-release\n" + f"SOLMARA_RELAY_RUNTIME_IMAGE=" + f"{self.module.SOLMARA_RELAY_RUNTIME_REPOSITORY}" + f"@sha256:{'3' * 64}\n", encoding="utf-8", ) @@ -152,6 +163,57 @@ def test_registryctl_version_must_match_release_tag(self) -> None: stderr.getvalue(), ) + def test_source_commit_must_match_resolved_release_tag(self) -> None: + self.module.resolve_tag_commit.return_value = "b" * 40 + stderr = io.StringIO() + with ( + mock.patch.object(self.module, "inspect_tag_digest") as inspect, + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) + + inspect.assert_not_called() + self.assertIn( + f"REGISTRY_STACK_SOURCE_COMMIT from versions.env is {'a' * 40}", + stderr.getvalue(), + ) + self.assertIn( + f"v1.0.0 resolves to {'b' * 40}", + stderr.getvalue(), + ) + + def test_resolve_tag_commit_prefers_the_peeled_annotated_tag(self) -> None: + direct = "1" * 40 + peeled = "2" * 40 + completed = subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=( + f"{direct}\trefs/tags/v1.0.0\n" + f"{peeled}\trefs/tags/v1.0.0^{{}}\n" + ), + stderr="", + ) + with mock.patch.object(self.module.subprocess, "run", return_value=completed): + self.assertEqual( + self.resolve_tag_implementation("v1.0.0"), + peeled, + ) + + def test_resolve_tag_commit_accepts_a_lightweight_tag(self) -> None: + direct = "3" * 40 + completed = subprocess.CompletedProcess( + args=[], + returncode=0, + stdout=f"{direct}\trefs/tags/v1.0.0\n", + stderr="", + ) + with mock.patch.object(self.module.subprocess, "run", return_value=completed): + self.assertEqual( + self.resolve_tag_implementation("v1.0.0"), + direct, + ) + def test_relay_source_ref_and_feature_must_match_the_release(self) -> None: versions = (self.root / "versions.env").read_text(encoding="utf-8") (self.root / "versions.env").write_text( @@ -181,6 +243,33 @@ def test_relay_source_ref_and_feature_must_match_the_release(self) -> None: stderr.getvalue(), ) + def test_relay_runtime_must_be_published_and_digest_pinned(self) -> None: + versions = (self.root / "versions.env").read_text(encoding="utf-8") + (self.root / "versions.env").write_text( + versions.replace( + ( + f"{self.module.SOLMARA_RELAY_RUNTIME_REPOSITORY}" + f"@sha256:{'3' * 64}" + ), + "solmara-lab-registry-relay:local", + ), + encoding="utf-8", + ) + stderr = io.StringIO() + with ( + mock.patch.object(self.module, "inspect_tag_digest") as inspect, + contextlib.redirect_stderr(stderr), + ): + self.assertEqual(self.module.main(["check-release-pins.py", "v1.0.0"]), 1) + + inspect.assert_not_called() + self.module.resolve_tag_commit.assert_not_called() + self.assertIn( + "SOLMARA_RELAY_RUNTIME_IMAGE must pin " + f"{self.module.SOLMARA_RELAY_RUNTIME_REPOSITORY}@sha256:", + stderr.getvalue(), + ) + def test_mismatched_environment_override_fails_before_registry_lookup(self) -> None: stderr = io.StringIO() with ( diff --git a/versions.env b/versions.env index b7e6acd..5422316 100644 --- a/versions.env +++ b/versions.env @@ -4,7 +4,8 @@ REGISTRYCTL_VERSION=0.13.0 REGISTRY_STACK_SOURCE_REF=v0.13.0 REGISTRY_STACK_SOURCE_COMMIT=985022e58c627967480b8ad304fbbd250ec7193d REGISTRY_RELAY_FEATURES=attribute-release -SOLMARA_RELAY_RUNTIME_IMAGE=solmara-lab-registry-relay:v0.13.0-attribute-release +SOLMARA_RELAY_RUNTIME_IMAGE=ghcr.io/registrystack/solmara-lab-relay-runtime@sha256:d4bc677ae1c01fbf788396b650f7252e5ed179d8e361f17ac0a0c800fa392ce5 +SOLMARA_RELAY_RUNTIME_DEV_IMAGE=solmara-lab-registry-relay:v0.13.0-attribute-release REGISTRY_RELAY_STATE_EPOCH=v013 POSTGRES_IMAGE=postgres:16-alpine STATIC_METADATA_IMAGE=python:3.12-alpine From 254eb8ba3cfce905b6c2850bb17391fbba1f8383 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 22:41:16 +0700 Subject: [PATCH 04/14] Test contract proof with published Relay runtime Signed-off-by: Jeremi Joslin --- scripts/contract-generation-proof.py | 13 ++++++++++ scripts/test_contract_generation_proof.py | 30 +++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/scripts/contract-generation-proof.py b/scripts/contract-generation-proof.py index 6021850..6c8ded1 100755 --- a/scripts/contract-generation-proof.py +++ b/scripts/contract-generation-proof.py @@ -37,6 +37,9 @@ MAX_DIAGNOSTIC_LINE_BYTES = 256 MAX_DIAGNOSTIC_BYTES = 4096 ANSI_ESCAPE = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]") +SOLMARA_RELAY_RUNTIME = re.compile( + r"ghcr\.io/registrystack/solmara-lab-relay-runtime@sha256:[0-9a-f]{64}" +) class ProofFailure(RuntimeError): @@ -57,6 +60,15 @@ def read_env(path: Path) -> dict[str, str]: return values +def relay_runtime_image(environment: Mapping[str, str]) -> str: + image = environment.get("SOLMARA_RELAY_RUNTIME_IMAGE", "") + if SOLMARA_RELAY_RUNTIME.fullmatch(image) is None: + raise ProofFailure( + "SOLMARA_RELAY_RUNTIME_IMAGE must pin the published feature runtime by digest" + ) + return image + + def diagnostic_environment(environment: Mapping[str, str] | None) -> dict[str, str]: return dict(os.environ if environment is None else environment) @@ -392,6 +404,7 @@ def main() -> int: environment.update(os.environ) environment.update( { + "REGISTRY_RELAY_IMAGE": relay_runtime_image(environment), "SOLMARA_POSTGRES_PORT": "0", "SOLMARA_SRO_RELAY_PORT": "0", "SOLMARA_SRO_NOTARY_PORT": "0", diff --git a/scripts/test_contract_generation_proof.py b/scripts/test_contract_generation_proof.py index 5b80bad..deedef6 100644 --- a/scripts/test_contract_generation_proof.py +++ b/scripts/test_contract_generation_proof.py @@ -50,6 +50,36 @@ def test_successor_is_a_revision_only_change(self) -> None: after = yaml.safe_load(integration.read_text(encoding="utf-8")) self.assertEqual(before | {"revision": 2}, after) + def test_proof_uses_the_digest_pinned_feature_runtime(self) -> None: + runtime = ( + "ghcr.io/registrystack/solmara-lab-relay-runtime@sha256:" + + "a" * 64 + ) + self.assertEqual( + self.proof.relay_runtime_image( + { + "REGISTRY_RELAY_IMAGE": ( + "ghcr.io/registrystack/registry-relay@sha256:" + "b" * 64 + ), + "SOLMARA_RELAY_RUNTIME_IMAGE": runtime, + } + ), + runtime, + ) + + def test_proof_rejects_a_mutable_feature_runtime_reference(self) -> None: + with self.assertRaisesRegex( + self.proof.ProofFailure, + "must pin the published feature runtime by digest", + ): + self.proof.relay_runtime_image( + { + "SOLMARA_RELAY_RUNTIME_IMAGE": ( + "ghcr.io/registrystack/solmara-lab-relay-runtime:v0.13.0" + ) + } + ) + def test_mixed_override_replaces_only_notary_generation(self) -> None: with tempfile.TemporaryDirectory() as directory: root = Path(directory) From 60ad6894e16d04987719163267f8db2c57deb953 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 22:45:33 +0700 Subject: [PATCH 05/14] Report contract proof bootstrap failures Signed-off-by: Jeremi Joslin --- scripts/contract-generation-proof.py | 52 ++++++++++++++++++----- scripts/test_contract_generation_proof.py | 34 +++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/scripts/contract-generation-proof.py b/scripts/contract-generation-proof.py index 6c8ded1..ef6dd26 100755 --- a/scripts/contract-generation-proof.py +++ b/scripts/contract-generation-proof.py @@ -268,6 +268,46 @@ def compose_command(project_name: str, override: Path | None = None) -> list[str return command +def start_generation( + compose: Sequence[str], environment: Mapping[str, str] +) -> None: + result = run( + [ + *compose, + "up", + "--detach", + "--wait", + "--wait-timeout", + "180", + "sro-notary", + ], + environment=environment, + timeout=300, + check=False, + ) + if result.returncode == 0: + return + logs = run( + [ + *compose, + "logs", + "--no-color", + "postgres", + "registry-postgresql-bootstrap", + "sro-relay-state-bootstrap", + ], + environment=environment, + timeout=30, + check=False, + ) + raise command_failure( + "docker", + result.returncode, + f"{result.stdout}\nfailed service logs:\n{logs.stdout}", + environment, + ) + + def wait_for_ready(url: str, timeout: int = 120) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: @@ -448,11 +488,7 @@ def main() -> int: try: print("contract-generation-proof: starting the complete blue generation") - run( - [*blue_compose, "up", "--detach", "--wait", "--wait-timeout", "180", "sro-notary"], - environment=environment, - timeout=300, - ) + start_generation(blue_compose, environment) blue_url = shared_notary_url(blue_compose, environment) wait_for_ready(blue_url) blue_response = evaluate(blue_url, token, BLUE_SUBJECT) @@ -512,11 +548,7 @@ def main() -> int: run([*blue_compose, "down", "--remove-orphans"], environment=environment) print("contract-generation-proof: activating the complete successor generation") - run( - [*green_compose, "up", "--detach", "--wait", "--wait-timeout", "180", "sro-notary"], - environment=environment, - timeout=300, - ) + start_generation(green_compose, environment) green_url = shared_notary_url(green_compose, environment) wait_for_ready(green_url) green_response = evaluate(green_url, token, GREEN_SUBJECT) diff --git a/scripts/test_contract_generation_proof.py b/scripts/test_contract_generation_proof.py index deedef6..af628ad 100644 --- a/scripts/test_contract_generation_proof.py +++ b/scripts/test_contract_generation_proof.py @@ -149,6 +149,40 @@ def test_failed_command_output_is_actionable_bounded_and_redacted(self) -> None: len(diagnostic.encode("utf-8")), self.proof.MAX_DIAGNOSTIC_BYTES ) + def test_failed_generation_reports_redacted_bootstrap_logs(self) -> None: + secret = "bootstrap-secret-that-must-not-appear" + compose = ["docker", "compose", "--project-name", "proof"] + with mock.patch.object( + self.proof, + "run", + side_effect=[ + subprocess.CompletedProcess(compose, 1, "service failed"), + subprocess.CompletedProcess( + compose, + 0, + f"sro-relay-state-bootstrap | migration rejected {secret}", + ), + ], + ) as mocked_run: + with self.assertRaises(self.proof.ProofFailure) as rejected: + self.proof.start_generation( + compose, + {"REGISTRY_RELAY_STATE_MIGRATION_PASSWORD": secret}, + ) + message = str(rejected.exception) + self.assertIn("failed service logs", message) + self.assertIn("migration rejected", message) + self.assertNotIn(secret, message) + self.assertEqual(mocked_run.call_count, 2) + self.assertEqual( + mocked_run.call_args_list[1].args[0][-3:], + [ + "postgres", + "registry-postgresql-bootstrap", + "sro-relay-state-bootstrap", + ], + ) + def test_cleanup_failures_are_fatal_without_a_primary_failure(self) -> None: failures = ( lambda: subprocess.CompletedProcess(["docker"], 1, "cleanup rejected"), From aaeee3f3dbb11f17fb6f473942fc413b19a37b27 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 22:50:03 +0700 Subject: [PATCH 06/14] Make contract proof configs runtime-readable Signed-off-by: Jeremi Joslin --- scripts/contract-generation-proof.py | 8 ++++++++ scripts/test_contract_generation_proof.py | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/scripts/contract-generation-proof.py b/scripts/contract-generation-proof.py index ef6dd26..4dd5dca 100755 --- a/scripts/contract-generation-proof.py +++ b/scripts/contract-generation-proof.py @@ -212,6 +212,12 @@ def make_successor(project: Path) -> None: integration.write_text(yaml.safe_dump(document, sort_keys=False), encoding="utf-8") +def make_runtime_readable(root: Path) -> None: + root.chmod(0o755) + for path in root.rglob("*"): + path.chmod(0o755 if path.is_dir() else 0o644) + + def build_generation(registryctl: Path, source: Path, destination: Path) -> str: run( [ @@ -229,6 +235,8 @@ def build_generation(registryctl: Path, source: Path, destination: Path) -> str: shutil.copytree(private / "relay" / "config", relay) notary.mkdir(parents=True) shutil.copy2(private / "notary" / "config" / "notary.yaml", notary / "notary.yaml") + make_runtime_readable(relay) + make_runtime_readable(notary) document = yaml.safe_load((notary / "notary.yaml").read_text(encoding="utf-8")) return document["evidence"]["claims"][0]["evidence_mode"]["consultations"]["household"]["profile"]["contract_hash"] diff --git a/scripts/test_contract_generation_proof.py b/scripts/test_contract_generation_proof.py index af628ad..72b063f 100644 --- a/scripts/test_contract_generation_proof.py +++ b/scripts/test_contract_generation_proof.py @@ -50,6 +50,25 @@ def test_successor_is_a_revision_only_change(self) -> None: after = yaml.safe_load(integration.read_text(encoding="utf-8")) self.assertEqual(before | {"revision": 2}, after) + def test_proof_copy_is_readable_by_the_unprivileged_runtime(self) -> None: + with tempfile.TemporaryDirectory() as directory: + runtime = Path(directory) / "runtime" + nested = runtime / "artifacts" + nested.mkdir(parents=True, mode=0o700) + config = runtime / "relay.yaml" + config.write_text("server: {}\n", encoding="utf-8") + config.chmod(0o600) + artifact = nested / "contract.json" + artifact.write_text("{}\n", encoding="utf-8") + artifact.chmod(0o600) + + self.proof.make_runtime_readable(runtime) + + self.assertEqual(runtime.stat().st_mode & 0o777, 0o755) + self.assertEqual(nested.stat().st_mode & 0o777, 0o755) + self.assertEqual(config.stat().st_mode & 0o777, 0o644) + self.assertEqual(artifact.stat().st_mode & 0o777, 0o644) + def test_proof_uses_the_digest_pinned_feature_runtime(self) -> None: runtime = ( "ghcr.io/registrystack/solmara-lab-relay-runtime@sha256:" From 12d60b3cbaa86cd6de744a471e2d938167c3969a Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 23:17:26 +0700 Subject: [PATCH 07/14] Link Relay runtime package to Solmara Signed-off-by: Jeremi Joslin --- docker/relay-runtime/Dockerfile | 1 + scripts/test_quality_scripts.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/docker/relay-runtime/Dockerfile b/docker/relay-runtime/Dockerfile index 0b453e0..933f451 100644 --- a/docker/relay-runtime/Dockerfile +++ b/docker/relay-runtime/Dockerfile @@ -22,6 +22,7 @@ ARG REGISTRY_RELAY_IMAGE ARG REGISTRY_STACK_SOURCE_COMMIT ARG REGISTRY_RELAY_FEATURES LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}" +LABEL org.opencontainers.image.source="https://github.com/registrystack/solmara-lab" LABEL org.opencontainers.image.revision="${REGISTRY_STACK_SOURCE_COMMIT}" LABEL org.registrystack.solmara.relay-features="${REGISTRY_RELAY_FEATURES}" diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index bb25108..6d8fa37 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -784,6 +784,10 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: 'LABEL org.opencontainers.image.base.name="${REGISTRY_RELAY_IMAGE}"', dockerfile, ) + self.assertIn( + 'LABEL org.opencontainers.image.source="https://github.com/registrystack/solmara-lab"', + dockerfile, + ) self.assertIn("--features \"${REGISTRY_RELAY_FEATURES}\"", dockerfile) self.assertNotIn("--all-features", dockerfile) From f1fc780104c75fd60a16fdae6dc4254c2e211808 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sat, 25 Jul 2026 23:30:07 +0700 Subject: [PATCH 08/14] Reuse published Relay runtime in release builds Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 21 ++++----------------- README.md | 22 ++++++++++++---------- docs/hosted-deployment.md | 11 ++++++----- scripts/test_quality_scripts.py | 23 +++++++++++++---------- 4 files changed, 35 insertions(+), 42 deletions(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 4ed2276..e7fd100 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -54,6 +54,7 @@ jobs: { 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 'REGISTRY_STACK_SOURCE_DIR=%s/vendor/registry-stack\n' "$GITHUB_WORKSPACE" } >> "$GITHUB_ENV" - name: Checkout eSignet relay authenticator @@ -106,21 +107,6 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Prepare feature-enabled Relay runtime build - run: cp docker/relay-runtime/Dockerfile vendor/registry-stack/Dockerfile.solmara-relay-runtime - - name: Build and push feature-enabled Relay runtime - id: relay-runtime - uses: docker/build-push-action@v6 - with: - context: vendor/registry-stack - file: vendor/registry-stack/Dockerfile.solmara-relay-runtime - platforms: linux/amd64 - push: true - tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay-runtime:${{ env.SOLMARA_IMAGE_TAG }} - build-args: | - REGISTRY_RELAY_IMAGE=${{ env.REGISTRY_RELAY_IMAGE }} - REGISTRY_STACK_SOURCE_COMMIT=${{ env.REGISTRY_STACK_SOURCE_COMMIT }} - REGISTRY_RELAY_FEATURES=${{ env.REGISTRY_RELAY_FEATURES }} - name: Build and push hosted Relay image uses: docker/build-push-action@v6 with: @@ -130,7 +116,7 @@ jobs: push: true tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }} build-args: | - REGISTRY_RELAY_IMAGE=${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay-runtime@${{ steps.relay-runtime.outputs.digest }} + REGISTRY_RELAY_IMAGE=${{ env.SOLMARA_RELAY_RUNTIME_IMAGE }} - name: Build and push hosted Notary image uses: docker/build-push-action@v6 with: @@ -231,7 +217,8 @@ jobs: { echo "### Solmara image digests" echo - for image in solmara-lab-relay-runtime 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 + 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}}')" echo "- ${SOLMARA_IMAGE_REGISTRY}/${image}@${digest}" diff --git a/README.md b/README.md index be336d0..669056f 100644 --- a/README.md +++ b/README.md @@ -107,11 +107,12 @@ committed generated artifact. `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 runtime base, while `just up` builds and caches a replacement -Relay binary from the pinned source with the explicitly declared -`attribute-release` feature. That opt-in is required by the NIA eSignet -profile because the release Relay intentionally excludes beta API features. -The build refuses a dirty or mismatched source checkout. +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 @@ -171,11 +172,12 @@ 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 verifies the pinned Registry Stack source, -builds and publishes the feature-enabled Relay runtime, then uses its immutable -digest as the base for the hosted Relay wrapper. It also builds the other -Solmara-owned images and writes their digest refs to the workflow summary for -Coolify env vars: +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`, `SOLMARA_PORTAL_IMAGE`, `SOLMARA_SCENARIO_RUNNER_IMAGE`, diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 5968774..3b302a7 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -75,11 +75,12 @@ Registry Stack Relay and Notary image refs are inputs to the Solmara wrapper builds. The `release-candidate` workflow requires a Registry Stack candidate or release tag and accepts only Relay and Notary input digests that match the committed `versions.env` pins. It also checks out the exact Registry Stack -source commit declared there and builds a Relay runtime with the -`attribute-release` feature required by the NIA eSignet profile. The published -release Relay remains the immutable runtime base. The feature runtime digest -then becomes the base of the deployable Solmara Relay wrapper. The workflow -reports immutable digest refs for these Coolify variables: +source commit declared there for release and contract verification. The +separately published Relay runtime enables the `attribute-release` feature +required by the NIA eSignet profile and is pinned by digest in `versions.env`. +The workflow uses that reviewed runtime directly as the base of the deployable +Solmara Relay wrapper instead of recompiling it for every candidate. It reports +the runtime pin and immutable digest refs for these Coolify variables: - `SOLMARA_RELAY_IMAGE` - `SOLMARA_NOTARY_IMAGE` diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index 6d8fa37..cd2c1f7 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -822,18 +822,21 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: release_workflow = ( ROOT / ".github" / "workflows" / "release-candidate.yml" ).read_text(encoding="utf-8") - prepare_build = release_workflow.index( - "- name: Prepare feature-enabled Relay runtime build" + self.assertNotIn( + "- name: Build and push feature-enabled Relay runtime", + release_workflow, ) - self.assertLess( - release_workflow.index("- name: Visitor center e2e"), - prepare_build, + self.assertIn( + "printf 'SOLMARA_RELAY_RUNTIME_IMAGE=%s", + release_workflow, ) - self.assertLess( - prepare_build, - release_workflow.index( - "- name: Build and push feature-enabled Relay runtime" - ), + self.assertIn( + "REGISTRY_RELAY_IMAGE=${{ env.SOLMARA_RELAY_RUNTIME_IMAGE }}", + release_workflow, + ) + self.assertIn( + 'echo "- ${SOLMARA_RELAY_RUNTIME_IMAGE}"', + release_workflow, ) def test_coolify_authority_state_is_postgresql_isolated(self) -> None: From 5504eff72a381bf424efcc9becd8b22700361924 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 00:11:40 +0700 Subject: [PATCH 09/14] Sign hosted Relay configuration bundles Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 21 ++ README.md | 7 + compose.coolify.agriculture.yaml | 29 +- compose.coolify.interior.yaml | 51 +++- compose.coolify.labour-pensions.yaml | 29 +- compose.coolify.social-development.yaml | 51 +++- .../cra-civil/antirollback-seed.json | 9 + .../cra-civil/bootstrap.yaml | 142 ++++++++++ .../cra-child-benefit-civil.json | 1 + .../cra-citizen-record-civil.json | 1 + .../cra-pension-death-civil.json | 1 + .../civil-person-by-uin/conformance.json | 1 + .../civil-person-by-uin/minimization.json | 1 + .../negative-security.json | 1 + .../civil-person-by-uin.json | 1 + .../cra-child-benefit-civil.json | 1 + .../cra-citizen-record-civil.json | 1 + .../cra-pension-death-civil.json | 1 + .../cra-civil/bundle/config/relay.yaml | 142 ++++++++++ .../cra-civil/bundle/manifest.json | 57 ++++ .../cra-civil/bundle/manifest.sig.json | 10 + .../cra-civil/trust-anchor.json | 20 ++ .../mosd-programme/antirollback-seed.json | 9 + .../mosd-programme/bootstrap.yaml | 122 ++++++++ .../child-benefit-enrollment.json | 1 + .../enrollment-by-uin/conformance.json | 1 + .../enrollment-by-uin/minimization.json | 1 + .../enrollment-by-uin/negative-security.json | 1 + .../integration-packs/enrollment-by-uin.json | 1 + .../child-benefit-enrollment.json | 1 + .../mosd-programme/bundle/config/relay.yaml | 122 ++++++++ .../mosd-programme/bundle/manifest.json | 41 +++ .../mosd-programme/bundle/manifest.sig.json | 10 + .../mosd-programme/trust-anchor.json | 20 ++ .../nagdi-agriculture/antirollback-seed.json | 9 + .../nagdi-agriculture/bootstrap.yaml | 238 ++++++++++++++++ .../livestock-herd.json | 1 + .../voucher-farmer.json | 1 + .../farmer-voucher-by-farmer/conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../farmer-voucher-by-farmer.json | 1 + .../livestock-movement-by-farmer.json | 1 + .../private-bindings/livestock-herd.json | 1 + .../private-bindings/voucher-farmer.json | 1 + .../bundle/config/relay.yaml | 238 ++++++++++++++++ .../nagdi-agriculture/bundle/manifest.json | 65 +++++ .../bundle/manifest.sig.json | 10 + .../nagdi-agriculture/trust-anchor.json | 20 ++ .../nia-population/antirollback-seed.json | 9 + .../nia-population/bootstrap.yaml | 261 ++++++++++++++++++ .../nia-child-benefit-population.json | 1 + .../nia-citizen-status-population.json | 1 + .../population-person-by-uin/conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../population-person-by-uin.json | 1 + .../nia-child-benefit-population.json | 1 + .../nia-citizen-status-population.json | 1 + .../nia-population/bundle/config/relay.yaml | 261 ++++++++++++++++++ .../nia-population/bundle/manifest.json | 49 ++++ .../nia-population/bundle/manifest.sig.json | 10 + .../nia-population/trust-anchor.json | 20 ++ .../sipf-pensions/antirollback-seed.json | 9 + .../sipf-pensions/bootstrap.yaml | 186 +++++++++++++ .../sipf-pension-payment-review-pension.json | 1 + .../sipf-survivor-benefit-survivor.json | 1 + .../conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../pension-case-by-survivor/conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../pension-case-by-pensioner.json | 1 + .../pension-case-by-survivor.json | 1 + .../sipf-pension-payment-review-pension.json | 1 + .../sipf-survivor-benefit-survivor.json | 1 + .../sipf-pensions/bundle/config/relay.yaml | 186 +++++++++++++ .../sipf-pensions/bundle/manifest.json | 65 +++++ .../sipf-pensions/bundle/manifest.sig.json | 10 + .../sipf-pensions/trust-anchor.json | 20 ++ .../sro-social/antirollback-seed.json | 9 + .../sro-social/bootstrap.yaml | 122 ++++++++ .../child-benefit-household.json | 1 + .../conformance.json | 1 + .../minimization.json | 1 + .../negative-security.json | 1 + .../child-benefit-household-by-uin.json | 1 + .../child-benefit-household.json | 1 + .../sro-social/bundle/config/relay.yaml | 122 ++++++++ .../sro-social/bundle/manifest.json | 41 +++ .../sro-social/bundle/manifest.sig.json | 10 + .../sro-social/trust-anchor.json | 20 ++ docker/relay/Dockerfile | 10 +- docs/hosted-deployment.md | 19 ++ justfile | 5 + scripts/check-config-secrets.py | 1 + scripts/check-hosted-relay-bundles.py | 150 ++++++++++ scripts/check-image-pins.py | 8 +- scripts/generate-hosted-relay-bundles.py | 210 ++++++++++++++ scripts/test_quality_scripts.py | 62 ++++- versions.env | 2 +- 105 files changed, 3376 insertions(+), 27 deletions(-) create mode 100644 config/hosted-relay-bundles/cra-civil/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/cra-civil/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/integration-packs/civil-person-by-uin.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/cra-civil/trust-anchor.json create mode 100644 config/hosted-relay-bundles/mosd-programme/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/integration-packs/enrollment-by-uin.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/mosd-programme/trust-anchor.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/livestock-herd.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/voucher-farmer.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/livestock-herd.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/voucher-farmer.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json create mode 100644 config/hosted-relay-bundles/nia-population/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/nia-population/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/integration-packs/population-person-by-uin.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/nia-population/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/nia-population/trust-anchor.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/sipf-pensions/trust-anchor.json create mode 100644 config/hosted-relay-bundles/sro-social/antirollback-seed.json create mode 100644 config/hosted-relay-bundles/sro-social/bootstrap.yaml create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/consultation-contracts/child-benefit-household.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/artifacts/private-bindings/child-benefit-household.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml create mode 100644 config/hosted-relay-bundles/sro-social/bundle/manifest.json create mode 100644 config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json create mode 100644 config/hosted-relay-bundles/sro-social/trust-anchor.json create mode 100755 scripts/check-hosted-relay-bundles.py create mode 100755 scripts/generate-hosted-relay-bundles.py diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index e7fd100..1ae5ceb 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -55,6 +55,7 @@ jobs: 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 @@ -117,6 +118,26 @@ jobs: tags: ${{ env.SOLMARA_IMAGE_REGISTRY }}/solmara-lab-relay:${{ env.SOLMARA_IMAGE_TAG }} build-args: | 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 + done - name: Build and push hosted Notary image uses: docker/build-push-action@v6 with: diff --git a/README.md b/README.md index 669056f..5e8e23d 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ 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 # compare committed versions.env pins against a candidate or release tag @@ -161,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. diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index 8531ff9..fc511d6 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -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: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -83,9 +90,23 @@ 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_${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}"] + 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} @@ -94,13 +115,17 @@ services: 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} diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index df04d86..1ea2e39 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -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: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -89,9 +96,37 @@ services: postgres: condition: service_healthy + cra-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/cra-civil/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: + - cra-relay-cache:/var/lib/registry-relay/cache + + nia-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/nia-population/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: + - nia-relay-cache:/var/lib/registry-relay/cache + cra-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/cra-civil/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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}"] + command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/cra-civil/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_cra_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: ${CRA_RELAY_AUDIT_HASH_SECRET} @@ -100,13 +135,17 @@ services: REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${CRA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${CRA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_cra_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + volumes: + - cra-relay-cache:/var/lib/registry-relay/cache depends_on: + cra-relay-config-state-init: + condition: service_completed_successfully registry-postgresql-bootstrap: condition: service_completed_successfully nia-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/nia-population/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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}"] + command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/nia-population/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_nia_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: ${NIA_RELAY_AUDIT_HASH_SECRET} @@ -116,13 +155,17 @@ services: REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${NIA_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${NIA_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_nia_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_NIA_DATABASE_URL: ${SOLMARA_NIA_DATABASE_URL} + volumes: + - nia-relay-cache:/var/lib/registry-relay/cache depends_on: + nia-relay-config-state-init: + condition: service_completed_successfully registry-postgresql-bootstrap: condition: service_completed_successfully cra-civil-relay: <<: *relay-common - command: ["--config", "/etc/solmara/registry-projects/hosted/cra-civil/relay/relay.yaml"] + command: ["--config", "/etc/solmara/hosted-relay-bundles/cra-civil/bootstrap.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${CRA_RELAY_AUDIT_HASH_SECRET} @@ -139,7 +182,7 @@ services: nia-population-relay: <<: *relay-common - command: ["--config", "/etc/solmara/registry-projects/hosted/nia-population/relay/relay.yaml"] + command: ["--config", "/etc/solmara/hosted-relay-bundles/nia-population/bootstrap.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${NIA_RELAY_AUDIT_HASH_SECRET} diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index 846ea65..71a2714 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -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: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -84,9 +91,23 @@ services: postgres: condition: service_healthy + sipf-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/sipf-pensions/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: + - sipf-relay-cache:/var/lib/registry-relay/cache + sipf-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/sipf-pensions/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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}"] + command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/sipf-pensions/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sipf_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: ${SIPF_RELAY_AUDIT_HASH_SECRET} @@ -96,13 +117,17 @@ services: REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SIPF_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SIPF_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sipf_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" SOLMARA_SIPF_DATABASE_URL: ${SOLMARA_SIPF_DATABASE_URL} + volumes: + - sipf-relay-cache:/var/lib/registry-relay/cache depends_on: + sipf-relay-config-state-init: + condition: service_completed_successfully registry-postgresql-bootstrap: condition: service_completed_successfully sipf-pensions-relay: <<: *relay-common - command: ["--config", "/etc/solmara/registry-projects/hosted/sipf-pensions/relay/relay.yaml"] + command: ["--config", "/etc/solmara/hosted-relay-bundles/sipf-pensions/bootstrap.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SIPF_RELAY_AUDIT_HASH_SECRET} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 6c469f4..3342419 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -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: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -88,9 +95,37 @@ services: postgres: condition: service_healthy + sro-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/sro-social/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: + - sro-relay-cache:/var/lib/registry-relay/cache + + programme-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/mosd-programme/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: + - programme-relay-cache:/var/lib/registry-relay/cache + sro-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/sro-social/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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}"] + command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/sro-social/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_sro_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: ${SRO_RELAY_AUDIT_HASH_SECRET} @@ -99,13 +134,17 @@ services: REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${SRO_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${SRO_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_sro_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + volumes: + - sro-relay-cache:/var/lib/registry-relay/cache depends_on: + sro-relay-config-state-init: + condition: service_completed_successfully registry-postgresql-bootstrap: condition: service_completed_successfully programme-relay-state-bootstrap: <<: *relay-bootstrap-common - command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/registry-projects/hosted/mosd-programme/relay/relay.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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}"] + command: ["consultation", "bootstrap-state", "--config", "/etc/solmara/hosted-relay-bundles/mosd-programme/bootstrap.yaml", "--migration-database-url-env", "REGISTRY_RELAY_STATE_MIGRATION_URL", "--owner-role", "solmara_relay_programme_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: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} @@ -114,13 +153,17 @@ services: REGISTRY_RELAY_STATE_MIGRATION_URL: "postgresql://${SOLMARA_POSTGRES_USER:-solmara_registry}:${SOLMARA_POSTGRES_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_MAINTENANCE_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_maintenance:${PROGRAMME_RELAY_POSTGRES_KEYRING_MAINTENANCE_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" REGISTRY_RELAY_STATE_KEYRING_READER_URL: "postgresql://solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}_keyring_reader:${PROGRAMME_RELAY_POSTGRES_KEYRING_READER_PASSWORD}@postgres:5432/solmara_relay_programme_consultation_${REGISTRY_RELAY_STATE_EPOCH:-v013}?sslmode=require" + volumes: + - programme-relay-cache:/var/lib/registry-relay/cache depends_on: + programme-relay-config-state-init: + condition: service_completed_successfully registry-postgresql-bootstrap: condition: service_completed_successfully sro-social-relay: <<: *relay-common - command: ["--config", "/etc/solmara/registry-projects/hosted/sro-social/relay/relay.yaml"] + command: ["--config", "/etc/solmara/hosted-relay-bundles/sro-social/bootstrap.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${SRO_RELAY_AUDIT_HASH_SECRET} @@ -137,7 +180,7 @@ services: programme-mis-relay: <<: *relay-common - command: ["--config", "/etc/solmara/registry-projects/hosted/mosd-programme/relay/relay.yaml"] + command: ["--config", "/etc/solmara/hosted-relay-bundles/mosd-programme/bootstrap.yaml"] environment: <<: *relay-environment REGISTRY_RELAY_AUDIT_HASH_SECRET: ${PROGRAMME_RELAY_AUDIT_HASH_SECRET} diff --git a/config/hosted-relay-bundles/cra-civil/antirollback-seed.json b/config/hosted-relay-bundles/cra-civil/antirollback-seed.json new file mode 100644 index 0000000..e06d6a0 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-cra-civil" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/cra-civil/bootstrap.yaml b/config/hosted-relay-bundles/cra-civil/bootstrap.yaml new file mode 100644 index 0000000..bf2d080 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bootstrap.yaml @@ -0,0 +1,142 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - cra-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://cra-relay.solmara.registrystack.org + publisher: solmara-cra-civil + title: solmara-cra-civil governed Registry Relay +consultation: + artifacts: + evidence: + - class: negative_security + path: artifacts/evidence/civil-person-by-uin/negative-security.json + sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f + - class: minimization + path: artifacts/evidence/civil-person-by-uin/minimization.json + sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 + - class: conformance + path: artifacts/evidence/civil-person-by-uin/conformance.json + sha256: sha256:89240aa696a2d5ca7c9ecb82c97d2c0820cdda99e08e2c7a6231421e6ac8243f + integration_packs: + - hash: sha256:f4920ecf6f90431d38b7fcedc4fd300f4ebe519fa5ead9cc9bec3815c82242cd + path: artifacts/integration-packs/civil-person-by-uin.json + sha256: sha256:f40e80c4b2438f07c01708e3734c6fd19921d1a141c0422fae3dcd93de11444e + private_bindings: + - hash: sha256:4842bc08b4fe9cfb64b666f304643fb045d6e36c0274c55258406d2781d4893a + path: artifacts/private-bindings/cra-child-benefit-civil.json + sha256: sha256:beaa10610496ebb358e67483555735813ccb2dc7489714ce59e1dc7f4803372f + - hash: sha256:59d48d92e98ba2b3397a586fb741be029c6eddc643db693b7f05f649f6a7d68c + path: artifacts/private-bindings/cra-citizen-record-civil.json + sha256: sha256:a5ee1310ec5308ff9408fb59c7f9cd7dceef6a6539b576e63a191a0635dfbc9c + - hash: sha256:3cde9994eaa584c8fe853590d3164b71fb0d5a1978db01c93eabff51d281c2e5 + path: artifacts/private-bindings/cra-pension-death-civil.json + sha256: sha256:6e6fdcddc1ddc8725f1364f8d88eee050c64f2cfe792b37500ab0b1d86b8073f + public_contracts: + - hash: sha256:fbedcba5fafa3efdf582785abf7cd5d39fb1bbd88db87d0e02a282c2efdf0dbf + path: artifacts/consultation-contracts/cra-child-benefit-civil.json + sha256: sha256:c557bd62579855c17a5fcfd756caaeb2ed5fe6606b49350b8249fd488e47b7cb + - hash: sha256:bf8efcce107cd31707d331a0f2ac8e6203beabb81959226d5fa5f52895319180 + path: artifacts/consultation-contracts/cra-citizen-record-civil.json + sha256: sha256:68f176b1f86e32312740310c17dcfda850312daeb471ad19d71d30f28d3e2c27 + - hash: sha256:4402a0e3b84825442d83febb8d65214031e98d39e13306f144786c9e17e3c451 + path: artifacts/consultation-contracts/cra-pension-death-civil.json + sha256: sha256:e9972065be18eedf1eb1e331531586d761f1b14b38d19103e5e3e35622596878 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: cra-notary + principal_id: cra-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 5222333210205388 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 3104283851243516304 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized civil_people entity + entities: [] + id: civil_people + owner: solmara-cra-civil + sensitivity: personal + tables: + - access: + aggregate_scope: civil_people:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224 + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: birth_brn + nullable: true + sensitive: false + type: string + - name: birth_date + nullable: false + sensitive: false + type: date + - name: deceased + nullable: false + sensitive: false + type: boolean + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv + type: file + title: civil_people + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: cra-civil-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json new file mode 100644 index 0000000..5cf568f --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-child-benefit-civil.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.cra-child-benefit.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:9ec50673e36783484c8e35137bd65396a871ecc12c2e8fdd2cc68b1f0cab108b","id":"relay.cra-child-benefit.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:cra-child-benefit","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json new file mode 100644 index 0000000..d80cd56 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-citizen-record-civil.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.cra-citizen-record.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ddf7a76ffe3dabacabdc1df18588bb87826a00a061572ad49c5052baf073f576","id":"relay.cra-citizen-record.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:cra-citizen-record","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json new file mode 100644 index 0000000..e729c12 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/consultation-contracts/cra-pension-death-civil.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.cra-pension-death.civil","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:dd59f03e034f68b35706ae6193bdd580081f2b596c6b92673fc7a66961e34fc1","id":"relay.cra-pension-death.civil","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:cra-pension-death","workload":"cra-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["birth_brn","birth_date","deceased"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json new file mode 100644 index 0000000..0496fc4 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":false,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2027-0101-00001","birth_date":"2027-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-future-birth-date","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":true,"child-age-under-5":true,"civil-record-linked":true,"person-is-deceased":false},"error":null,"outcome":"match","outputs":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"birth_brn":"BRN-2022-0101-00001","birth_date":"2022-03-14","deceased":false},"headers":{},"status":200}}}],"name":"cra-civil-person-match","variables":{"as_of_date":"2026-07-14"}},{"classification":"synthetic","expect":{"claims":{"birth-is-registered":false,"child-age-under-5":false,"civil-record-linked":false,"person-is-deceased":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"cra-civil-person-no-match","variables":{"as_of_date":"2026-07-14"}}],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json new file mode 100644 index 0000000..9d92e3c --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"solmara-cra-civil-person-by-uin","operations":{},"outputs":{"birth_brn":{"from":"snapshot.record.birth_brn","max_bytes":128,"maximum":null,"minimum":null,"nullable":true,"type":"string"},"birth_date":{"from":"snapshot.record.birth_date","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"date"},"deceased":{"from":"snapshot.record.deceased","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json new file mode 100644 index 0000000..18ba42a --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/evidence/civil-person-by-uin/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"solmara-cra-civil-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/integration-packs/civil-person-by-uin.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/integration-packs/civil-person-by-uin.json new file mode 100644 index 0000000..f0db9fe --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/integration-packs/civil-person-by-uin.json @@ -0,0 +1 @@ +{"id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:89240aa696a2d5ca7c9ecb82c97d2c0820cdda99e08e2c7a6231421e6ac8243f"],"minimization":["sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263"],"negative_security":["sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f"]},"input_slots":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"solmara-cra-civil-person-by-uin","output":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"max_bytes":10,"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"birth_brn":{"max_bytes":128,"nullable":true,"type":"string"},"birth_date":{"nullable":false,"type":"date"},"deceased":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json new file mode 100644 index 0000000..63cd972 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-child-benefit-civil.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4920ecf6f90431d38b7fcedc4fd300f4ebe519fa5ead9cc9bec3815c82242cd","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224"},"profile":{"id":"solmara-cra-civil.cra-child-benefit.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json new file mode 100644 index 0000000..ea4118e --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-citizen-record-civil.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4920ecf6f90431d38b7fcedc4fd300f4ebe519fa5ead9cc9bec3815c82242cd","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224"},"profile":{"id":"solmara-cra-civil.cra-citizen-record.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json new file mode 100644 index 0000000..bf1a7a0 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/artifacts/private-bindings/cra-pension-death-civil.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f4920ecf6f90431d38b7fcedc4fd300f4ebe519fa5ead9cc9bec3815c82242cd","id":"solmara-cra-civil.solmara-cra-civil-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"birth_brn":"birth_brn","birth_date":"birth_date","deceased":"deceased"}},"table_provider":"civil_people__materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224"},"profile":{"id":"solmara-cra-civil.cra-pension-death.civil","version":"1"},"registry_instance":"solmara-cra-civil","source_instance":"materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224","tenant":"solmara-cra-civil"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml b/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml new file mode 100644 index 0000000..bf2d080 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/config/relay.yaml @@ -0,0 +1,142 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - cra-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://cra-relay.solmara.registrystack.org + publisher: solmara-cra-civil + title: solmara-cra-civil governed Registry Relay +consultation: + artifacts: + evidence: + - class: negative_security + path: artifacts/evidence/civil-person-by-uin/negative-security.json + sha256: sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f + - class: minimization + path: artifacts/evidence/civil-person-by-uin/minimization.json + sha256: sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263 + - class: conformance + path: artifacts/evidence/civil-person-by-uin/conformance.json + sha256: sha256:89240aa696a2d5ca7c9ecb82c97d2c0820cdda99e08e2c7a6231421e6ac8243f + integration_packs: + - hash: sha256:f4920ecf6f90431d38b7fcedc4fd300f4ebe519fa5ead9cc9bec3815c82242cd + path: artifacts/integration-packs/civil-person-by-uin.json + sha256: sha256:f40e80c4b2438f07c01708e3734c6fd19921d1a141c0422fae3dcd93de11444e + private_bindings: + - hash: sha256:4842bc08b4fe9cfb64b666f304643fb045d6e36c0274c55258406d2781d4893a + path: artifacts/private-bindings/cra-child-benefit-civil.json + sha256: sha256:beaa10610496ebb358e67483555735813ccb2dc7489714ce59e1dc7f4803372f + - hash: sha256:59d48d92e98ba2b3397a586fb741be029c6eddc643db693b7f05f649f6a7d68c + path: artifacts/private-bindings/cra-citizen-record-civil.json + sha256: sha256:a5ee1310ec5308ff9408fb59c7f9cd7dceef6a6539b576e63a191a0635dfbc9c + - hash: sha256:3cde9994eaa584c8fe853590d3164b71fb0d5a1978db01c93eabff51d281c2e5 + path: artifacts/private-bindings/cra-pension-death-civil.json + sha256: sha256:6e6fdcddc1ddc8725f1364f8d88eee050c64f2cfe792b37500ab0b1d86b8073f + public_contracts: + - hash: sha256:fbedcba5fafa3efdf582785abf7cd5d39fb1bbd88db87d0e02a282c2efdf0dbf + path: artifacts/consultation-contracts/cra-child-benefit-civil.json + sha256: sha256:c557bd62579855c17a5fcfd756caaeb2ed5fe6606b49350b8249fd488e47b7cb + - hash: sha256:bf8efcce107cd31707d331a0f2ac8e6203beabb81959226d5fa5f52895319180 + path: artifacts/consultation-contracts/cra-citizen-record-civil.json + sha256: sha256:68f176b1f86e32312740310c17dcfda850312daeb471ad19d71d30f28d3e2c27 + - hash: sha256:4402a0e3b84825442d83febb8d65214031e98d39e13306f144786c9e17e3c451 + path: artifacts/consultation-contracts/cra-pension-death-civil.json + sha256: sha256:e9972065be18eedf1eb1e331531586d761f1b14b38d19103e5e3e35622596878 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: cra-notary + principal_id: cra-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 5222333210205388 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 3104283851243516304 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized civil_people entity + entities: [] + id: civil_people + owner: solmara-cra-civil + sensitivity: personal + tables: + - access: + aggregate_scope: civil_people:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_0da8728e995c98d62fa7369cb2a3326ba819664a06fde658101496b4bc3df224 + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: birth_brn + nullable: true + sensitive: false + type: string + - name: birth_date + nullable: false + sensitive: false + type: date + - name: deceased + nullable: false + sensitive: false + type: boolean + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/interior-civil/fixtures/civil_person_projection.csv + type: file + title: civil_people + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: cra-civil-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/cra-civil/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/cra-civil/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/cra-civil/bundle/manifest.json b/config/hosted-relay-bundles/cra-civil/bundle/manifest.json new file mode 100644 index 0000000..07ed5a4 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/manifest.json @@ -0,0 +1,57 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-cra-civil", + "instance_id": "cra-civil-relay", + "bundle_id": "solmara-hosted-cra-civil-sequence-1", + "sequence": 1, + "config_hash": "sha256:f9553f2b560a722b11c4ed16e35221c52246059498dfe3a53f865d6c1a0f3b6c", + "files": [ + { + "path": "config/artifacts/consultation-contracts/cra-child-benefit-civil.json", + "sha256": "sha256:c557bd62579855c17a5fcfd756caaeb2ed5fe6606b49350b8249fd488e47b7cb" + }, + { + "path": "config/artifacts/consultation-contracts/cra-citizen-record-civil.json", + "sha256": "sha256:68f176b1f86e32312740310c17dcfda850312daeb471ad19d71d30f28d3e2c27" + }, + { + "path": "config/artifacts/consultation-contracts/cra-pension-death-civil.json", + "sha256": "sha256:e9972065be18eedf1eb1e331531586d761f1b14b38d19103e5e3e35622596878" + }, + { + "path": "config/artifacts/evidence/civil-person-by-uin/conformance.json", + "sha256": "sha256:89240aa696a2d5ca7c9ecb82c97d2c0820cdda99e08e2c7a6231421e6ac8243f" + }, + { + "path": "config/artifacts/evidence/civil-person-by-uin/minimization.json", + "sha256": "sha256:5fa0459553307e10c55a7613981afdc2a42c0ea02e2bb93fe76ad6f2d2fcd263" + }, + { + "path": "config/artifacts/evidence/civil-person-by-uin/negative-security.json", + "sha256": "sha256:2613360dc2b35b1e02aa276bc52ff7ff35fc083dc4713d1036df405eb3523f0f" + }, + { + "path": "config/artifacts/integration-packs/civil-person-by-uin.json", + "sha256": "sha256:f40e80c4b2438f07c01708e3734c6fd19921d1a141c0422fae3dcd93de11444e" + }, + { + "path": "config/artifacts/private-bindings/cra-child-benefit-civil.json", + "sha256": "sha256:beaa10610496ebb358e67483555735813ccb2dc7489714ce59e1dc7f4803372f" + }, + { + "path": "config/artifacts/private-bindings/cra-citizen-record-civil.json", + "sha256": "sha256:a5ee1310ec5308ff9408fb59c7f9cd7dceef6a6539b576e63a191a0635dfbc9c" + }, + { + "path": "config/artifacts/private-bindings/cra-pension-death-civil.json", + "sha256": "sha256:6e6fdcddc1ddc8725f1364f8d88eee050c64f2cfe792b37500ab0b1d86b8073f" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:f9553f2b560a722b11c4ed16e35221c52246059498dfe3a53f865d6c1a0f3b6c" + } + ], + "created_at": "2026-07-25T17:00:39.189454Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json b/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json new file mode 100644 index 0000000..453d988 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "FE4yXIufpVeQ4fZRjdU9OMRfnCSU7DmUg1v1Il78wpKzqtohpbOAvUTQiAEYRJo2pzYZ2LNun_7wavICXuarBw" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/cra-civil/trust-anchor.json b/config/hosted-relay-bundles/cra-civil/trust-anchor.json new file mode 100644 index 0000000..8a96601 --- /dev/null +++ b/config/hosted-relay-bundles/cra-civil/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-cra-civil", + "instance_id": "cra-civil-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json b/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json new file mode 100644 index 0000000..818f505 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-mosd-programme" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml b/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml new file mode 100644 index 0000000..04074b6 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bootstrap.yaml @@ -0,0 +1,122 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - programme-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://mosd-programme-relay.solmara.registrystack.org + publisher: solmara-mosd-programme + title: solmara-mosd-programme governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/enrollment-by-uin/minimization.json + sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 + - class: negative_security + path: artifacts/evidence/enrollment-by-uin/negative-security.json + sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 + - class: conformance + path: artifacts/evidence/enrollment-by-uin/conformance.json + sha256: sha256:be306951c683bbe6b02eff0202e8f4ed78c7a4379bc921d3b2f5e0ed0fcad2e4 + integration_packs: + - hash: sha256:f3c7c1d244dd12ff23ec92de6b44b50adc7195e1fed39bc7226d022eb4194284 + path: artifacts/integration-packs/enrollment-by-uin.json + sha256: sha256:056cb318d4c9a49271f5283d52dfa282ff351af8b480d88c74eeaa6de7dcee33 + private_bindings: + - hash: sha256:0cb86f544d14128977aeb6b69b43d9700c4668c395c8b1a790750e4225463cbe + path: artifacts/private-bindings/child-benefit-enrollment.json + sha256: sha256:e92f9f56ef5027c5a9292628d1d96f7a9c313496e21f6200d8e512b2a1d31778 + public_contracts: + - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 + path: artifacts/consultation-contracts/child-benefit-enrollment.json + sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: programme-notary + principal_id: programme-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 9069287079765840165 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 891105660075992367 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized mosd_enroll entity + entities: [] + id: mosd_enroll + owner: solmara-mosd-programme + sensitivity: personal + tables: + - access: + aggregate_scope: mosd_enroll:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: duplicate_flag + nullable: false + sensitive: false + type: boolean + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv + type: file + title: mosd_enroll + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: programme-mis-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json new file mode 100644 index 0000000..bc41921 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/consultation-contracts/child-benefit-enrollment.json @@ -0,0 +1 @@ +{"id":"solmara-mosd-programme.child-benefit.enrollment","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:50a4f9fd40526b837aa90b02d27e13e98f66d9de1362eb06fb2465afbae31c53","id":"relay.child-benefit.enrollment","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"programme-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-mosd-programme.enrollment-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["duplicate_flag"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json new file mode 100644 index 0000000..e5df568 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":true},"error":null,"outcome":"match","outputs":{"duplicate_flag":false}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"duplicate_flag":false},"headers":{},"status":200}}}],"name":"enrollment-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"not-already-enrolled":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"enrollment-no-match","variables":{}}],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json new file mode 100644 index 0000000..1d539e7 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"enrollment-by-uin","operations":{},"outputs":{"duplicate_flag":{"from":"snapshot.record.duplicate_flag","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json new file mode 100644 index 0000000..0b38f83 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/evidence/enrollment-by-uin/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"enrollment-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/integration-packs/enrollment-by-uin.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/integration-packs/enrollment-by-uin.json new file mode 100644 index 0000000..8326d97 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/integration-packs/enrollment-by-uin.json @@ -0,0 +1 @@ +{"id":"solmara-mosd-programme.enrollment-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:be306951c683bbe6b02eff0202e8f4ed78c7a4379bc921d3b2f5e0ed0fcad2e4"],"minimization":["sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83"],"negative_security":["sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"enrollment-by-uin","output":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"duplicate_flag":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json new file mode 100644 index 0000000..99ade87 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/artifacts/private-bindings/child-benefit-enrollment.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:f3c7c1d244dd12ff23ec92de6b44b50adc7195e1fed39bc7226d022eb4194284","id":"solmara-mosd-programme.enrollment-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"duplicate_flag":"duplicate_flag"}},"table_provider":"mosd_enroll__materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf"},"profile":{"id":"solmara-mosd-programme.child-benefit.enrollment","version":"1"},"registry_instance":"solmara-mosd-programme","source_instance":"materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf","tenant":"solmara-mosd-programme"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml b/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml new file mode 100644 index 0000000..04074b6 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/config/relay.yaml @@ -0,0 +1,122 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - programme-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://mosd-programme-relay.solmara.registrystack.org + publisher: solmara-mosd-programme + title: solmara-mosd-programme governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/enrollment-by-uin/minimization.json + sha256: sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83 + - class: negative_security + path: artifacts/evidence/enrollment-by-uin/negative-security.json + sha256: sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282 + - class: conformance + path: artifacts/evidence/enrollment-by-uin/conformance.json + sha256: sha256:be306951c683bbe6b02eff0202e8f4ed78c7a4379bc921d3b2f5e0ed0fcad2e4 + integration_packs: + - hash: sha256:f3c7c1d244dd12ff23ec92de6b44b50adc7195e1fed39bc7226d022eb4194284 + path: artifacts/integration-packs/enrollment-by-uin.json + sha256: sha256:056cb318d4c9a49271f5283d52dfa282ff351af8b480d88c74eeaa6de7dcee33 + private_bindings: + - hash: sha256:0cb86f544d14128977aeb6b69b43d9700c4668c395c8b1a790750e4225463cbe + path: artifacts/private-bindings/child-benefit-enrollment.json + sha256: sha256:e92f9f56ef5027c5a9292628d1d96f7a9c313496e21f6200d8e512b2a1d31778 + public_contracts: + - hash: sha256:9da47bc7171991c51e098b44cb0667595b344d5f4c2b994a67f0c3b401d4b5b3 + path: artifacts/consultation-contracts/child-benefit-enrollment.json + sha256: sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: programme-notary + principal_id: programme-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 9069287079765840165 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 891105660075992367 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized mosd_enroll entity + entities: [] + id: mosd_enroll + owner: solmara-mosd-programme + sensitivity: personal + tables: + - access: + aggregate_scope: mosd_enroll:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_5f676d9c112ce16b7ede827eab122868889c2a4c9a1d4b38a214beecadc245bf + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: duplicate_flag + nullable: false + sensitive: false + type: boolean + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/social-development/fixtures/programme_mis_enrollment.csv + type: file + title: mosd_enroll + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: programme-mis-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/mosd-programme/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/mosd-programme/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json new file mode 100644 index 0000000..b5a7f72 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.json @@ -0,0 +1,41 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-mosd-programme", + "instance_id": "programme-mis-relay", + "bundle_id": "solmara-hosted-mosd-programme-sequence-1", + "sequence": 1, + "config_hash": "sha256:ca2a5a971a0836a07a45d90fd34eddfc2d3f0fa7ee81f4e383a4904a68d10867", + "files": [ + { + "path": "config/artifacts/consultation-contracts/child-benefit-enrollment.json", + "sha256": "sha256:253ee934a9c9bbf725e233f90c195d057893eeea14bc9bc12330c0957245ff10" + }, + { + "path": "config/artifacts/evidence/enrollment-by-uin/conformance.json", + "sha256": "sha256:be306951c683bbe6b02eff0202e8f4ed78c7a4379bc921d3b2f5e0ed0fcad2e4" + }, + { + "path": "config/artifacts/evidence/enrollment-by-uin/minimization.json", + "sha256": "sha256:2d57d4d6bf1f97aba14b447b84f844af70791a8eac8bff7be2cc72a4aa3fce83" + }, + { + "path": "config/artifacts/evidence/enrollment-by-uin/negative-security.json", + "sha256": "sha256:5468291382c61d128d2aff47dfbb7151ae1736c72d64b7a69e1209cd67d95282" + }, + { + "path": "config/artifacts/integration-packs/enrollment-by-uin.json", + "sha256": "sha256:056cb318d4c9a49271f5283d52dfa282ff351af8b480d88c74eeaa6de7dcee33" + }, + { + "path": "config/artifacts/private-bindings/child-benefit-enrollment.json", + "sha256": "sha256:e92f9f56ef5027c5a9292628d1d96f7a9c313496e21f6200d8e512b2a1d31778" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:ca2a5a971a0836a07a45d90fd34eddfc2d3f0fa7ee81f4e383a4904a68d10867" + } + ], + "created_at": "2026-07-25T17:00:39.322248Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json new file mode 100644 index 0000000..6e017e2 --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "zIyotaCnQYnLPeUDgBAIpgtmBh6eRfu711UPbOxDTuMcOvHBJt2NyVhJGqxoWofysko2K7iHXFg3D5DhHMDsCg" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/mosd-programme/trust-anchor.json b/config/hosted-relay-bundles/mosd-programme/trust-anchor.json new file mode 100644 index 0000000..492ed9d --- /dev/null +++ b/config/hosted-relay-bundles/mosd-programme/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-mosd-programme", + "instance_id": "programme-mis-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json b/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json new file mode 100644 index 0000000..69a1383 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nagdi-agriculture" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml b/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml new file mode 100644 index 0000000..6d1a318 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bootstrap.yaml @@ -0,0 +1,238 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - nagdi-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://nagdi-relay.solmara.registrystack.org + publisher: solmara-nagdi-agriculture + title: solmara-nagdi-agriculture governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/livestock-movement-by-farmer/minimization.json + sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 + - class: conformance + path: artifacts/evidence/livestock-movement-by-farmer/conformance.json + sha256: sha256:35117ed74d495cea90b696965796062183770f6f6481ffefc8924cae1031ea24 + - class: conformance + path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json + sha256: sha256:96bcc904223d22fb7bcbf27018dab660a1c0f5f58c7a08d53a6c1b43c6d9ca3f + - class: negative_security + path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json + sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d + - class: negative_security + path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json + sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce + - class: minimization + path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json + sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b + integration_packs: + - hash: sha256:11c8536f96c8ee6487c211b22ad6808969a50cae11c1defe4224031edacd31dc + path: artifacts/integration-packs/farmer-voucher-by-farmer.json + sha256: sha256:e5db33fd90a72478b892fb7bd20573cf46631b6c0b034c2d1c1241bae8d86071 + - hash: sha256:0997815c7c98ffc7abfe5d012715cc04daafc2eea687ccc0e72590451df16655 + path: artifacts/integration-packs/livestock-movement-by-farmer.json + sha256: sha256:b6236bae451a7ea0491832d5e5ddcd41576cd31c541e730be649b6d71d361f8e + private_bindings: + - hash: sha256:203f86fbf07249314ec758dccb2608e6266ecea300d3d3111af233899bb8e2e3 + path: artifacts/private-bindings/livestock-herd.json + sha256: sha256:a8e862bb3199df9bc08cf365bae0152130667b79e0e5d504b6d7f6a5c866d5b8 + - hash: sha256:8d6757bca5d17bd633d0521330fbbe42ede14c2b9e6bc09cd3ad75267677f774 + path: artifacts/private-bindings/voucher-farmer.json + sha256: sha256:b06356849df57a97cafdc5071e134d2dded1a08be7b63e878c92222ad03778ed + public_contracts: + - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 + path: artifacts/consultation-contracts/livestock-herd.json + sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c + - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c + path: artifacts/consultation-contracts/voucher-farmer.json + sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: nagdi-notary + principal_id: nagdi-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4554467022379962639 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 7450858971948805253 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized nagdi_farmer entity + entities: [] + id: nagdi_farmer + owner: solmara-nagdi-agriculture + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_farmer:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + materialization: snapshot + primary_key: farmer_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: active_farm_parcel + nullable: false + sensitive: false + type: boolean + - name: active_smallholder_farmer + nullable: false + sensitive: false + type: boolean + - name: crop_declared_for_season + nullable: false + sensitive: false + type: boolean + - name: data_use_authorized + nullable: false + sensitive: false + type: boolean + - name: district_climate_risk_active + nullable: false + sensitive: false + type: boolean + - name: farmer_id + nullable: false + sensitive: false + type: string + - name: farmer_registered + nullable: false + sensitive: false + type: boolean + - name: voucher_entitlement_current + nullable: false + sensitive: false + type: boolean + - name: voucher_not_redeemed + nullable: false + sensitive: false + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv + type: file + title: nagdi_farmer + update_frequency: as_needed +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized nagdi_herd entity + entities: [] + id: nagdi_herd + owner: solmara-nagdi-agriculture + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_herd:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + materialization: snapshot + primary_key: herd_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: destination_district_open + nullable: false + sensitive: false + type: boolean + - name: farmer_id + nullable: false + sensitive: false + type: string + - name: herd_id + nullable: false + sensitive: false + type: string + - name: herd_vaccination_current + nullable: false + sensitive: false + type: boolean + - name: no_conflicting_open_movement_permit + nullable: false + sensitive: false + type: boolean + - name: origin_district_not_quarantined_for_species + nullable: false + sensitive: false + type: boolean + - name: registered_herd + nullable: false + sensitive: false + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv + type: file + title: nagdi_herd + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: nagdi-agriculture-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/livestock-herd.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/livestock-herd.json new file mode 100644 index 0000000..adf39dd --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/livestock-herd.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.livestock.herd","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:05746b9128c73f3a079b88d4a6a5c392ea4d317755aa8e0771e0c83325a16b28","id":"relay.livestock.herd","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/livestock-movement-control"],"required_scope":"registry:consult:livestock","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["destination_district_open","herd_vaccination_current","no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species","registered_herd"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/voucher-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/voucher-farmer.json new file mode 100644 index 0000000..3f414ad --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/consultation-contracts/voucher-farmer.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.voucher.farmer","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"agricultural-data-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:6c9b13e328828747f1d32991be3ab0681112c283dae9f614a7187c8ca0d72a01","id":"relay.voucher.farmer","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/voucher-eligibility-review"],"required_scope":"registry:consult:voucher","workload":"nagdi-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["active_farm_parcel","active_smallholder_farmer","crop_declared_for_season","data_use_authorized","district_climate_risk_active","farmer_registered","voucher_entitlement_current","voucher_not_redeemed"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":134217728,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":604800000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json new file mode 100644 index 0000000..51e1184 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":true,"eligible-for-climate-smart-input-voucher":true,"farmer-registered":true},"error":null,"outcome":"match","outputs":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"active_farm_parcel":true,"active_smallholder_farmer":true,"crop_declared_for_season":true,"data_use_authorized":true,"district_climate_risk_active":true,"farmer_registered":true,"voucher_entitlement_current":true,"voucher_not_redeemed":true},"headers":{},"status":200}}}],"name":"farmer-voucher-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"data-use-authorized-for-purpose":false,"eligible-for-climate-smart-input-voucher":false,"farmer-registered":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"farmer-voucher-no-match","variables":{}}],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json new file mode 100644 index 0000000..59e808a --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"farmer-voucher-by-farmer","operations":{},"outputs":{"active_farm_parcel":{"from":"snapshot.record.active_farm_parcel","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"from":"snapshot.record.active_smallholder_farmer","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"crop_declared_for_season":{"from":"snapshot.record.crop_declared_for_season","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"data_use_authorized":{"from":"snapshot.record.data_use_authorized","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"district_climate_risk_active":{"from":"snapshot.record.district_climate_risk_active","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"farmer_registered":{"from":"snapshot.record.farmer_registered","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"from":"snapshot.record.voucher_entitlement_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"from":"snapshot.record.voucher_not_redeemed","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json new file mode 100644 index 0000000..7dfbf8c --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"farmer-voucher-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json new file mode 100644 index 0000000..81fcba7 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"farmer_id":"FR-8888"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},{"destination_district_open":false,"herd_vaccination_current":false,"no_conflicting_open_movement_permit":false,"origin_district_not_quarantined_for_species":false,"registered_herd":false}],"headers":{},"status":200}}}],"name":"livestock-movement-ambiguous","variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":true,"origin-district-not-quarantined-for-species":true,"registered-herd":true},"error":null,"outcome":"match","outputs":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true}},"input":{"farmer_id":"FR-1001"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"destination_district_open":true,"herd_vaccination_current":true,"no_conflicting_open_movement_permit":true,"origin_district_not_quarantined_for_species":true,"registered_herd":true},"headers":{},"status":200}}}],"name":"livestock-movement-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"eligible-for-livestock-movement-permit":false,"origin-district-not-quarantined-for-species":false,"registered-herd":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"farmer_id":"FR-9999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"livestock-movement-no-match","variables":{}}],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json new file mode 100644 index 0000000..6768a09 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"livestock-movement-by-farmer","operations":{},"outputs":{"destination_district_open":{"from":"snapshot.record.destination_district_open","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"herd_vaccination_current":{"from":"snapshot.record.herd_vaccination_current","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"from":"snapshot.record.no_conflicting_open_movement_permit","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"from":"snapshot.record.origin_district_not_quarantined_for_species","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"registered_herd":{"from":"snapshot.record.registered_herd","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json new file mode 100644 index 0000000..a823e60 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"livestock-movement-by-farmer","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json new file mode 100644 index 0000000..2674284 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/farmer-voucher-by-farmer.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:96bcc904223d22fb7bcbf27018dab660a1c0f5f58c7a08d53a6c1b43c6d9ca3f"],"minimization":["sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b"],"negative_security":["sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"farmer-voucher-by-farmer","output":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"active_farm_parcel":{"nullable":false,"type":"boolean"},"active_smallholder_farmer":{"nullable":false,"type":"boolean"},"crop_declared_for_season":{"nullable":false,"type":"boolean"},"data_use_authorized":{"nullable":false,"type":"boolean"},"district_climate_risk_active":{"nullable":false,"type":"boolean"},"farmer_registered":{"nullable":false,"type":"boolean"},"voucher_entitlement_current":{"nullable":false,"type":"boolean"},"voucher_not_redeemed":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json new file mode 100644 index 0000000..e3e2834 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/integration-packs/livestock-movement-by-farmer.json @@ -0,0 +1 @@ +{"id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:35117ed74d495cea90b696965796062183770f6f6481ffefc8924cae1031ea24"],"minimization":["sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5"],"negative_security":["sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce"]},"input_slots":{"farmer_id":{"maxLength":16,"pattern":"^FR-[0-9]+$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"livestock-movement-by-farmer","output":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":604800000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"destination_district_open":{"nullable":false,"type":"boolean"},"herd_vaccination_current":{"nullable":false,"type":"boolean"},"no_conflicting_open_movement_permit":{"nullable":false,"type":"boolean"},"origin_district_not_quarantined_for_species":{"nullable":false,"type":"boolean"},"registered_herd":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"farmer_id":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/livestock-herd.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/livestock-herd.json new file mode 100644 index 0000000..a6b965c --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/livestock-herd.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:0997815c7c98ffc7abfe5d012715cc04daafc2eea687ccc0e72590451df16655","id":"solmara-nagdi-agriculture.livestock-movement-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"destination_district_open":"destination_district_open","herd_vaccination_current":"herd_vaccination_current","no_conflicting_open_movement_permit":"no_conflicting_open_movement_permit","origin_district_not_quarantined_for_species":"origin_district_not_quarantined_for_species","registered_herd":"registered_herd"}},"table_provider":"nagdi_herd__materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b"},"profile":{"id":"solmara-nagdi-agriculture.livestock.herd","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/voucher-farmer.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/voucher-farmer.json new file mode 100644 index 0000000..9c2605b --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/artifacts/private-bindings/voucher-farmer.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:11c8536f96c8ee6487c211b22ad6808969a50cae11c1defe4224031edacd31dc","id":"solmara-nagdi-agriculture.farmer-voucher-by-farmer","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"farmer_id":{"comparison":"binary_equality","input":"farmer_id","physical_field":"farmer_id","physical_type":"utf8"}},"projection":{"active_farm_parcel":"active_farm_parcel","active_smallholder_farmer":"active_smallholder_farmer","crop_declared_for_season":"crop_declared_for_season","data_use_authorized":"data_use_authorized","district_climate_risk_active":"district_climate_risk_active","farmer_registered":"farmer_registered","voucher_entitlement_current":"voucher_entitlement_current","voucher_not_redeemed":"voucher_not_redeemed"}},"table_provider":"nagdi_farmer__materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7"},"profile":{"id":"solmara-nagdi-agriculture.voucher.farmer","version":"1"},"registry_instance":"solmara-nagdi-agriculture","source_instance":"materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7","tenant":"solmara-nagdi-agriculture"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml new file mode 100644 index 0000000..6d1a318 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/config/relay.yaml @@ -0,0 +1,238 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - nagdi-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://nagdi-relay.solmara.registrystack.org + publisher: solmara-nagdi-agriculture + title: solmara-nagdi-agriculture governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/livestock-movement-by-farmer/minimization.json + sha256: sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5 + - class: conformance + path: artifacts/evidence/livestock-movement-by-farmer/conformance.json + sha256: sha256:35117ed74d495cea90b696965796062183770f6f6481ffefc8924cae1031ea24 + - class: conformance + path: artifacts/evidence/farmer-voucher-by-farmer/conformance.json + sha256: sha256:96bcc904223d22fb7bcbf27018dab660a1c0f5f58c7a08d53a6c1b43c6d9ca3f + - class: negative_security + path: artifacts/evidence/farmer-voucher-by-farmer/negative-security.json + sha256: sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d + - class: negative_security + path: artifacts/evidence/livestock-movement-by-farmer/negative-security.json + sha256: sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce + - class: minimization + path: artifacts/evidence/farmer-voucher-by-farmer/minimization.json + sha256: sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b + integration_packs: + - hash: sha256:11c8536f96c8ee6487c211b22ad6808969a50cae11c1defe4224031edacd31dc + path: artifacts/integration-packs/farmer-voucher-by-farmer.json + sha256: sha256:e5db33fd90a72478b892fb7bd20573cf46631b6c0b034c2d1c1241bae8d86071 + - hash: sha256:0997815c7c98ffc7abfe5d012715cc04daafc2eea687ccc0e72590451df16655 + path: artifacts/integration-packs/livestock-movement-by-farmer.json + sha256: sha256:b6236bae451a7ea0491832d5e5ddcd41576cd31c541e730be649b6d71d361f8e + private_bindings: + - hash: sha256:203f86fbf07249314ec758dccb2608e6266ecea300d3d3111af233899bb8e2e3 + path: artifacts/private-bindings/livestock-herd.json + sha256: sha256:a8e862bb3199df9bc08cf365bae0152130667b79e0e5d504b6d7f6a5c866d5b8 + - hash: sha256:8d6757bca5d17bd633d0521330fbbe42ede14c2b9e6bc09cd3ad75267677f774 + path: artifacts/private-bindings/voucher-farmer.json + sha256: sha256:b06356849df57a97cafdc5071e134d2dded1a08be7b63e878c92222ad03778ed + public_contracts: + - hash: sha256:9eef29eec9341e5425a81d9c835232293f68a88652008793b2045e1711e6ef60 + path: artifacts/consultation-contracts/livestock-herd.json + sha256: sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c + - hash: sha256:8c72e6f499969307d6d4bce11dbdf493ead8306880f359ab458e55f2e2abde0c + path: artifacts/consultation-contracts/voucher-farmer.json + sha256: sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: nagdi-notary + principal_id: nagdi-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4554467022379962639 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 7450858971948805253 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized nagdi_farmer entity + entities: [] + id: nagdi_farmer + owner: solmara-nagdi-agriculture + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_farmer:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_a5df746e609cc2000dcc08b086dce4fbd407a4908364552aa2ea32be9d99c6f7 + materialization: snapshot + primary_key: farmer_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: active_farm_parcel + nullable: false + sensitive: false + type: boolean + - name: active_smallholder_farmer + nullable: false + sensitive: false + type: boolean + - name: crop_declared_for_season + nullable: false + sensitive: false + type: boolean + - name: data_use_authorized + nullable: false + sensitive: false + type: boolean + - name: district_climate_risk_active + nullable: false + sensitive: false + type: boolean + - name: farmer_id + nullable: false + sensitive: false + type: string + - name: farmer_registered + nullable: false + sensitive: false + type: boolean + - name: voucher_entitlement_current + nullable: false + sensitive: false + type: boolean + - name: voucher_not_redeemed + nullable: false + sensitive: false + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/farmer_voucher.csv + type: file + title: nagdi_farmer + update_frequency: as_needed +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized nagdi_herd entity + entities: [] + id: nagdi_herd + owner: solmara-nagdi-agriculture + sensitivity: personal + tables: + - access: + aggregate_scope: nagdi_herd:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_c530010904da8a151e2b3437cbb64d2c54dcbc140b0fcf9d6331e97e73063e9b + materialization: snapshot + primary_key: herd_id + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: destination_district_open + nullable: false + sensitive: false + type: boolean + - name: farmer_id + nullable: false + sensitive: false + type: string + - name: herd_id + nullable: false + sensitive: false + type: string + - name: herd_vaccination_current + nullable: false + sensitive: false + type: boolean + - name: no_conflicting_open_movement_permit + nullable: false + sensitive: false + type: boolean + - name: origin_district_not_quarantined_for_species + nullable: false + sensitive: false + type: boolean + - name: registered_herd + nullable: false + sensitive: false + type: boolean + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/agriculture-nagdi/fixtures/livestock_movement.csv + type: file + title: nagdi_herd + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: nagdi-agriculture-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/nagdi-agriculture/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json new file mode 100644 index 0000000..4cc10e0 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.json @@ -0,0 +1,65 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nagdi-agriculture", + "instance_id": "nagdi-agriculture-relay", + "bundle_id": "solmara-hosted-nagdi-agriculture-sequence-1", + "sequence": 1, + "config_hash": "sha256:26aad73db285acfce50e15045bb12c895dff9a967579eb1658756af8f3b2d4e1", + "files": [ + { + "path": "config/artifacts/consultation-contracts/livestock-herd.json", + "sha256": "sha256:a1f4045c36462881aec560fcf103c1f78252bf0e2acf81ddd6b75ce21ccfd84c" + }, + { + "path": "config/artifacts/consultation-contracts/voucher-farmer.json", + "sha256": "sha256:8ea73ba2779ef33a85cc5f34988fe0d14f13f1892baa185d0d5c67e76dcbe1ad" + }, + { + "path": "config/artifacts/evidence/farmer-voucher-by-farmer/conformance.json", + "sha256": "sha256:96bcc904223d22fb7bcbf27018dab660a1c0f5f58c7a08d53a6c1b43c6d9ca3f" + }, + { + "path": "config/artifacts/evidence/farmer-voucher-by-farmer/minimization.json", + "sha256": "sha256:ff7da0d595c08d92a85ca406da498fb54b49046332466b28e8fd960912e9a32b" + }, + { + "path": "config/artifacts/evidence/farmer-voucher-by-farmer/negative-security.json", + "sha256": "sha256:aee98b658e32ced730958f390ab158801d7c0023420f476067aaf5a7e8d58e0d" + }, + { + "path": "config/artifacts/evidence/livestock-movement-by-farmer/conformance.json", + "sha256": "sha256:35117ed74d495cea90b696965796062183770f6f6481ffefc8924cae1031ea24" + }, + { + "path": "config/artifacts/evidence/livestock-movement-by-farmer/minimization.json", + "sha256": "sha256:260f25e125f7e98d53c4daff92af0b92a6a3a3992f17914782cef9b2441356c5" + }, + { + "path": "config/artifacts/evidence/livestock-movement-by-farmer/negative-security.json", + "sha256": "sha256:db2275b39b446020d10e7b729bf9d5c1c3e73651b5019f60809ef2af55d789ce" + }, + { + "path": "config/artifacts/integration-packs/farmer-voucher-by-farmer.json", + "sha256": "sha256:e5db33fd90a72478b892fb7bd20573cf46631b6c0b034c2d1c1241bae8d86071" + }, + { + "path": "config/artifacts/integration-packs/livestock-movement-by-farmer.json", + "sha256": "sha256:b6236bae451a7ea0491832d5e5ddcd41576cd31c541e730be649b6d71d361f8e" + }, + { + "path": "config/artifacts/private-bindings/livestock-herd.json", + "sha256": "sha256:a8e862bb3199df9bc08cf365bae0152130667b79e0e5d504b6d7f6a5c866d5b8" + }, + { + "path": "config/artifacts/private-bindings/voucher-farmer.json", + "sha256": "sha256:b06356849df57a97cafdc5071e134d2dded1a08be7b63e878c92222ad03778ed" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:26aad73db285acfce50e15045bb12c895dff9a967579eb1658756af8f3b2d4e1" + } + ], + "created_at": "2026-07-25T17:00:39.410208Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json new file mode 100644 index 0000000..f03e8c9 --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "HH5--0M0GfG5zxAT9g45HYF-p9Jt6ltnvYnF_r2o6btJG6idXDLB0bMxoBgjPAF2AsEq5N6tLVvHydrh1sOpCg" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json b/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json new file mode 100644 index 0000000..efd067c --- /dev/null +++ b/config/hosted-relay-bundles/nagdi-agriculture/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nagdi-agriculture", + "instance_id": "nagdi-agriculture-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/antirollback-seed.json b/config/hosted-relay-bundles/nia-population/antirollback-seed.json new file mode 100644 index 0000000..eced2eb --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nia-population" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/nia-population/bootstrap.yaml b/config/hosted-relay-bundles/nia-population/bootstrap.yaml new file mode 100644 index 0000000..60e2375 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bootstrap.yaml @@ -0,0 +1,261 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - nia-notary + - solmara-esignet + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://nia-relay.solmara.registrystack.org + publisher: solmara-nia-population + title: solmara-nia-population governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/population-person-by-uin/minimization.json + sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 + - class: negative_security + path: artifacts/evidence/population-person-by-uin/negative-security.json + sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab + - class: conformance + path: artifacts/evidence/population-person-by-uin/conformance.json + sha256: sha256:db5fc1ce9ffa8420ca317c0fb27439f9e06431a4fc499e72b4d205110c76fe46 + integration_packs: + - hash: sha256:b1b41ad5dc077760ecac87296a135143261b8c80aac97bde43f1d78ddcadaa83 + path: artifacts/integration-packs/population-person-by-uin.json + sha256: sha256:1be1f2b32c7f7cd3e8454b1b221dcaa35b1850eabfc5be663c65b7ba4b6c0b76 + private_bindings: + - hash: sha256:4d0a784835f3accc07493a891715e02cdb59261ba6753ee5d73fc19ac025689a + path: artifacts/private-bindings/nia-child-benefit-population.json + sha256: sha256:8e3ab3b92c5d5a5d5d02b69f934a6c93b875f6a605a5ec7cab886120d1e7d4df + - hash: sha256:2240c9128075b26ae2445cf9feb85697e3a7e2be10fa329dd10cdbf8482e77bd + path: artifacts/private-bindings/nia-citizen-status-population.json + sha256: sha256:75a6d8ca2f1127b23d8f224916a7a5e13b88c56a3acc087d0587b47a7e889b05 + public_contracts: + - hash: sha256:7163d2e5e636f41478b1b2de6dbd4812d1ff8956c012133b6ac86c4b7e3c08ef + path: artifacts/consultation-contracts/nia-child-benefit-population.json + sha256: sha256:c314219b2980b2bfdad3b4e97aa1527ef87a8c43ff42fb9e5ec2ee89b7ef03fe + - hash: sha256:675d2c746c5b75755b76a563523ea43fcf24bc6def2dcaf993a5b06c76900ccb + path: artifacts/consultation-contracts/nia-citizen-status-population.json + sha256: sha256:a613d3ef4b07413f60a664af693b7420a224552d5342c165e7379cd59d206303 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: nia-notary + principal_id: nia-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 1016450306001272100 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6539803953464966811 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Governed population attributes used by NIA-owned services. + entities: + - access: + aggregate_scope: population:aggregate + evidence_verification_scope: population:evidence_verification + metadata_scope: population:metadata + read_scope: population:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: legacy_nid + ops: + - eq + - field: uin + ops: + - eq + default_limit: 25 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 100 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: + - claims: + - expression: null + name: birthdate + required: true + sensitivity: personal + source_field: birth_date + - expression: null + name: family_name + required: true + sensitivity: direct_identifier + source_field: family_name + - expression: null + name: gender + required: false + sensitivity: personal + source_field: sex + - expression: null + name: given_name + required: true + sensitivity: direct_identifier + source_field: given_name + - expression: null + name: individual_id + required: true + sensitivity: direct_identifier + source_field: uin + - expression: + cel: source.given_name + ' ' + source.family_name + name: name + required: true + sensitivity: direct_identifier + source_field: null + description: Minimal identity attributes for the Solmara eSignet authenticator. + id: solmara-nia-userinfo + purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification + release_conditions: + expression: + cel: source.identity_status == 'active' && source.alive == true + release_scope: population:identity_release + response: + include_source_metadata: false + max_age_seconds: 300 + subject: + cardinality: one + id_type: national_id + input: individual_id + source_field: uin + title: SolmaraID UserInfo + version: v1 + description: Governed population attributes used by NIA-owned services. + fields: + - from: alive + name: alive + sensitive: false + - from: birth_date + name: birth_date + sensitive: false + - from: family_name + name: family_name + sensitive: false + - from: given_name + name: given_name + sensitive: false + - from: identity_status + name: identity_status + sensitive: false + - from: legacy_nid + name: legacy_nid + sensitive: false + - from: sex + name: sex + sensitive: false + - from: uin + name: uin + sensitive: false + name: population + relationships: [] + spatial: null + table: materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0 + title: NIA population records + id: population + owner: National Identity Agency + sensitivity: personal + tables: + - access: + aggregate_scope: population:aggregate + metadata_scope: population:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 25 + max_limit: 100 + require_purpose_header: true + id: materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0 + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: alive + nullable: false + sensitive: false + type: boolean + - name: birth_date + nullable: false + sensitive: false + type: date + - name: family_name + nullable: false + sensitive: false + type: string + - name: given_name + nullable: false + sensitive: false + type: string + - name: identity_status + nullable: false + sensitive: false + type: string + - name: legacy_nid + nullable: true + sensitive: false + type: string + - name: sex + nullable: false + sensitive: false + type: string + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + connection_env: SOLMARA_NIA_DATABASE_URL + table: + name: population_person + schema: public + type: postgres + title: NIA population records + update_frequency: continuous +deployment: + profile: hosted_lab +instance: + environment: hosted + id: nia-population-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json new file mode 100644 index 0000000..dad9389 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-child-benefit-population.json @@ -0,0 +1 @@ +{"id":"solmara-nia-population.nia-child-benefit.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:7af974a445cf4ebaec93647f7d6aa80732d70829150702ab0891c06f91850359","id":"relay.nia-child-benefit.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:nia-child-benefit","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json new file mode 100644 index 0000000..0c4d5c6 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/consultation-contracts/nia-citizen-status-population.json @@ -0,0 +1 @@ +{"id":"solmara-nia-population.nia-citizen-status.population","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"citizen-self-service","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:0f648848f4934f37c2dbce9e58040e2ce7b43d39a5d1c8d0391cdd4aa07b9834","id":"relay.nia-citizen-status.population","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/citizen-self-service"],"required_scope":"registry:consult:nia-citizen-status","workload":"nia-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["alive","identity_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":536870912,"max_source_records":10000000},"immutable_generation":true,"max_snapshot_age_ms":300000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json new file mode 100644 index 0000000..24fe526 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":true,"population-record-active":true},"error":null,"outcome":"match","outputs":{"alive":true,"identity_status":"active"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"alive":true,"identity_status":"active"},"headers":{},"status":200}}}],"name":"nia-population-person-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"citizen-population-record-active":false,"population-record-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"nia-population-person-no-match","variables":{}}],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json new file mode 100644 index 0000000..223b391 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"solmara-nia-population-person-by-uin","operations":{},"outputs":{"alive":{"from":"snapshot.record.alive","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"},"identity_status":{"from":"snapshot.record.identity_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json new file mode 100644 index 0000000..53dcfa4 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/evidence/population-person-by-uin/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"solmara-nia-population-person-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/integration-packs/population-person-by-uin.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/integration-packs/population-person-by-uin.json new file mode 100644 index 0000000..ab72c00 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/integration-packs/population-person-by-uin.json @@ -0,0 +1 @@ +{"id":"solmara-nia-population.solmara-nia-population-person-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:db5fc1ce9ffa8420ca317c0fb27439f9e06431a4fc499e72b4d205110c76fe46"],"minimization":["sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407"],"negative_security":["sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab"]},"input_slots":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"solmara-nia-population-person-by-uin","output":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":300000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"alive":{"nullable":false,"type":"boolean"},"identity_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json new file mode 100644 index 0000000..53b6dde --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-child-benefit-population.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:b1b41ad5dc077760ecac87296a135143261b8c80aac97bde43f1d78ddcadaa83","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0"},"profile":{"id":"solmara-nia-population.nia-child-benefit.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json new file mode 100644 index 0000000..ea5e04c --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/artifacts/private-bindings/nia-citizen-status-population.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:b1b41ad5dc077760ecac87296a135143261b8c80aac97bde43f1d78ddcadaa83","id":"solmara-nia-population.solmara-nia-population-person-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"alive":"alive","identity_status":"identity_status"}},"table_provider":"population__materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0"},"profile":{"id":"solmara-nia-population.nia-citizen-status.population","version":"1"},"registry_instance":"solmara-nia-population","source_instance":"materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0","tenant":"solmara-nia-population"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml b/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml new file mode 100644 index 0000000..60e2375 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/config/relay.yaml @@ -0,0 +1,261 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - nia-notary + - solmara-esignet + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://nia-relay.solmara.registrystack.org + publisher: solmara-nia-population + title: solmara-nia-population governed Registry Relay +consultation: + artifacts: + evidence: + - class: minimization + path: artifacts/evidence/population-person-by-uin/minimization.json + sha256: sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407 + - class: negative_security + path: artifacts/evidence/population-person-by-uin/negative-security.json + sha256: sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab + - class: conformance + path: artifacts/evidence/population-person-by-uin/conformance.json + sha256: sha256:db5fc1ce9ffa8420ca317c0fb27439f9e06431a4fc499e72b4d205110c76fe46 + integration_packs: + - hash: sha256:b1b41ad5dc077760ecac87296a135143261b8c80aac97bde43f1d78ddcadaa83 + path: artifacts/integration-packs/population-person-by-uin.json + sha256: sha256:1be1f2b32c7f7cd3e8454b1b221dcaa35b1850eabfc5be663c65b7ba4b6c0b76 + private_bindings: + - hash: sha256:4d0a784835f3accc07493a891715e02cdb59261ba6753ee5d73fc19ac025689a + path: artifacts/private-bindings/nia-child-benefit-population.json + sha256: sha256:8e3ab3b92c5d5a5d5d02b69f934a6c93b875f6a605a5ec7cab886120d1e7d4df + - hash: sha256:2240c9128075b26ae2445cf9feb85697e3a7e2be10fa329dd10cdbf8482e77bd + path: artifacts/private-bindings/nia-citizen-status-population.json + sha256: sha256:75a6d8ca2f1127b23d8f224916a7a5e13b88c56a3acc087d0587b47a7e889b05 + public_contracts: + - hash: sha256:7163d2e5e636f41478b1b2de6dbd4812d1ff8956c012133b6ac86c4b7e3c08ef + path: artifacts/consultation-contracts/nia-child-benefit-population.json + sha256: sha256:c314219b2980b2bfdad3b4e97aa1527ef87a8c43ff42fb9e5ec2ee89b7ef03fe + - hash: sha256:675d2c746c5b75755b76a563523ea43fcf24bc6def2dcaf993a5b06c76900ccb + path: artifacts/consultation-contracts/nia-citizen-status-population.json + sha256: sha256:a613d3ef4b07413f60a664af693b7420a224552d5342c165e7379cd59d206303 + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: nia-notary + principal_id: nia-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 1016450306001272100 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6539803953464966811 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Governed population attributes used by NIA-owned services. + entities: + - access: + aggregate_scope: population:aggregate + evidence_verification_scope: population:evidence_verification + metadata_scope: population:metadata + read_scope: population:rows + aggregates: [] + api: + allowed_expansions: [] + allowed_filters: + - field: legacy_nid + ops: + - eq + - field: uin + ops: + - eq + default_limit: 25 + governed_policy: + allowed_assurance: [] + permitted_jurisdictions: [] + permitted_purposes: + - https://id.registrystack.org/solmara/purpose/esignet-identity-verification + redaction_fields: [] + require_consent: false + require_legal_basis: false + trusted_context: {} + max_limit: 100 + require_purpose_header: true + required_filter_bindings: [] + required_filters: [] + attribute_release_profiles: + - claims: + - expression: null + name: birthdate + required: true + sensitivity: personal + source_field: birth_date + - expression: null + name: family_name + required: true + sensitivity: direct_identifier + source_field: family_name + - expression: null + name: gender + required: false + sensitivity: personal + source_field: sex + - expression: null + name: given_name + required: true + sensitivity: direct_identifier + source_field: given_name + - expression: null + name: individual_id + required: true + sensitivity: direct_identifier + source_field: uin + - expression: + cel: source.given_name + ' ' + source.family_name + name: name + required: true + sensitivity: direct_identifier + source_field: null + description: Minimal identity attributes for the Solmara eSignet authenticator. + id: solmara-nia-userinfo + purpose: https://id.registrystack.org/solmara/purpose/esignet-identity-verification + release_conditions: + expression: + cel: source.identity_status == 'active' && source.alive == true + release_scope: population:identity_release + response: + include_source_metadata: false + max_age_seconds: 300 + subject: + cardinality: one + id_type: national_id + input: individual_id + source_field: uin + title: SolmaraID UserInfo + version: v1 + description: Governed population attributes used by NIA-owned services. + fields: + - from: alive + name: alive + sensitive: false + - from: birth_date + name: birth_date + sensitive: false + - from: family_name + name: family_name + sensitive: false + - from: given_name + name: given_name + sensitive: false + - from: identity_status + name: identity_status + sensitive: false + - from: legacy_nid + name: legacy_nid + sensitive: false + - from: sex + name: sex + sensitive: false + - from: uin + name: uin + sensitive: false + name: population + relationships: [] + spatial: null + table: materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0 + title: NIA population records + id: population + owner: National Identity Agency + sensitivity: personal + tables: + - access: + aggregate_scope: population:aggregate + metadata_scope: population:metadata + aggregates: [] + api: + allowed_filters: [] + default_limit: 25 + max_limit: 100 + require_purpose_header: true + id: materialization_57a06dc3eeac7d91ac0e8c80266789cb06395acd32f35a2f106a980f23733ab0 + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: alive + nullable: false + sensitive: false + type: boolean + - name: birth_date + nullable: false + sensitive: false + type: date + - name: family_name + nullable: false + sensitive: false + type: string + - name: given_name + nullable: false + sensitive: false + type: string + - name: identity_status + nullable: false + sensitive: false + type: string + - name: legacy_nid + nullable: true + sensitive: false + type: string + - name: sex + nullable: false + sensitive: false + type: string + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + connection_env: SOLMARA_NIA_DATABASE_URL + table: + name: population_person + schema: public + type: postgres + title: NIA population records + update_frequency: continuous +deployment: + profile: hosted_lab +instance: + environment: hosted + id: nia-population-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/nia-population/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/nia-population/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/nia-population/bundle/manifest.json b/config/hosted-relay-bundles/nia-population/bundle/manifest.json new file mode 100644 index 0000000..70e5179 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/manifest.json @@ -0,0 +1,49 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nia-population", + "instance_id": "nia-population-relay", + "bundle_id": "solmara-hosted-nia-population-sequence-1", + "sequence": 1, + "config_hash": "sha256:014776607d310c92d3c9decc9c89d08da237607843ff2f39d6be1215ae2c3401", + "files": [ + { + "path": "config/artifacts/consultation-contracts/nia-child-benefit-population.json", + "sha256": "sha256:c314219b2980b2bfdad3b4e97aa1527ef87a8c43ff42fb9e5ec2ee89b7ef03fe" + }, + { + "path": "config/artifacts/consultation-contracts/nia-citizen-status-population.json", + "sha256": "sha256:a613d3ef4b07413f60a664af693b7420a224552d5342c165e7379cd59d206303" + }, + { + "path": "config/artifacts/evidence/population-person-by-uin/conformance.json", + "sha256": "sha256:db5fc1ce9ffa8420ca317c0fb27439f9e06431a4fc499e72b4d205110c76fe46" + }, + { + "path": "config/artifacts/evidence/population-person-by-uin/minimization.json", + "sha256": "sha256:af6daeecb8d517d40d9b631abf938dceac510f1a6fb9da8eebbb29034c2a2407" + }, + { + "path": "config/artifacts/evidence/population-person-by-uin/negative-security.json", + "sha256": "sha256:bc4aecd3a77aa8f7f92699b4a39b5bd43e760fae1d4243856a6f1457781806ab" + }, + { + "path": "config/artifacts/integration-packs/population-person-by-uin.json", + "sha256": "sha256:1be1f2b32c7f7cd3e8454b1b221dcaa35b1850eabfc5be663c65b7ba4b6c0b76" + }, + { + "path": "config/artifacts/private-bindings/nia-child-benefit-population.json", + "sha256": "sha256:8e3ab3b92c5d5a5d5d02b69f934a6c93b875f6a605a5ec7cab886120d1e7d4df" + }, + { + "path": "config/artifacts/private-bindings/nia-citizen-status-population.json", + "sha256": "sha256:75a6d8ca2f1127b23d8f224916a7a5e13b88c56a3acc087d0587b47a7e889b05" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:014776607d310c92d3c9decc9c89d08da237607843ff2f39d6be1215ae2c3401" + } + ], + "created_at": "2026-07-25T17:00:39.243911Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json b/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json new file mode 100644 index 0000000..386eda3 --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "sVip4WL4YZpVQydimW1HwjQ1JFj8exGAwaBC1EJWpmshkvRb-jxZ4HZlmd9LU2VpgD1O1sYd636vAuk9NbIeCg" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/nia-population/trust-anchor.json b/config/hosted-relay-bundles/nia-population/trust-anchor.json new file mode 100644 index 0000000..29aec0b --- /dev/null +++ b/config/hosted-relay-bundles/nia-population/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-nia-population", + "instance_id": "nia-population-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json b/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json new file mode 100644 index 0000000..bc046ab --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sipf-pensions" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml b/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml new file mode 100644 index 0000000..e905e7b --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bootstrap.yaml @@ -0,0 +1,186 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - sipf-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://sipf-relay.solmara.registrystack.org + publisher: solmara-sipf-pensions + title: solmara-sipf-pensions governed Registry Relay +consultation: + artifacts: + evidence: + - class: conformance + path: artifacts/evidence/pension-case-by-survivor/conformance.json + sha256: sha256:036a62f001ad4028a5c732f10cbea5c09bbb8c42fdd2bdc40675942bf7fd990f + - class: minimization + path: artifacts/evidence/pension-case-by-survivor/minimization.json + sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f + - class: negative_security + path: artifacts/evidence/pension-case-by-survivor/negative-security.json + sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 + - class: minimization + path: artifacts/evidence/pension-case-by-pensioner/minimization.json + sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 + - class: conformance + path: artifacts/evidence/pension-case-by-pensioner/conformance.json + sha256: sha256:835002ccbffb2a459808c4d2da2b7a631d7d6f0940c0aa73b0c7169cfb04299c + - class: negative_security + path: artifacts/evidence/pension-case-by-pensioner/negative-security.json + sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d + integration_packs: + - hash: sha256:2bbd5327759f179a1a80324c5b134f43f6339781138547b3bb61ed8d06ffcbda + path: artifacts/integration-packs/pension-case-by-pensioner.json + sha256: sha256:c2991a8c90ecfaad79f28d60801f0a2772b0851d44a6ddc09fdcb1cc16498750 + - hash: sha256:4b59cd1e953bcddf378da658fefd38bc2483ca92bd186907e7423880582dd9b3 + path: artifacts/integration-packs/pension-case-by-survivor.json + sha256: sha256:0e5d17671c4bb70f2de35ebd92dc7fc745fa96539c89aaecb823014ea570b0c0 + private_bindings: + - hash: sha256:2afe86389a95b7cfc4c044cef4121eb76ef8d16e53062d23fc94e506147acd4a + path: artifacts/private-bindings/sipf-pension-payment-review-pension.json + sha256: sha256:a61426e41ec476090f81cad40af7c2cd841e78f656e95d0e59c28447772f2579 + - hash: sha256:ab18913ddee896c64080fb81c28753f17b5ceaf497e96d4f23b182ffe591ab4b + path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json + sha256: sha256:e073578d218287585af339adda4092ff7e3545f52ac7000c515e38acad1c4791 + public_contracts: + - hash: sha256:47ee6aa3f5503173cd06fa791843849c421e818295dc74c4bbdbd7cdb2c753e4 + path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json + sha256: sha256:21eca329ac5c7d87291eff868247acd763a56a15920ce7a23a7b8b5dd29753ec + - hash: sha256:ede60666de002be1c1aae9279375c4b5e02171582c042b4d61ab6186a1e1c5d5 + path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json + sha256: sha256:718a57529960909aa4904412ae1010544352817a16db8bd3670a9f4baca4e52f + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: sipf-notary + principal_id: sipf-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4444991830243562918 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6117019989203477116 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized pension entity + entities: [] + id: pension + owner: solmara-sipf-pensions + sensitivity: personal + tables: + - access: + aggregate_scope: pension:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_5b9c250beca9c627d5300e4bb7e831a5ff2ebc184c98142f4145e1bcdf0b42d7 + materialization: snapshot + primary_key: pensioner_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: payment_status + nullable: false + sensitive: false + type: string + - name: pensioner_uin + nullable: false + sensitive: false + type: string + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_pension_payment + schema: public + type: postgres + title: pension + update_frequency: as_needed +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized survivor entity + entities: [] + id: survivor + owner: solmara-sipf-pensions + sensitivity: personal + tables: + - access: + aggregate_scope: survivor:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_4469c4516b8b74da71d74914415251a07533406a6a6e1bbec963a302934d88d0 + materialization: snapshot + primary_key: spouse_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: spouse_uin + nullable: false + sensitive: false + type: string + - name: survivor_eligible + nullable: false + sensitive: false + type: boolean + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_survivor_benefit + schema: public + type: postgres + title: survivor + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: sipf-pensions-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json new file mode 100644 index 0000000..77bae94 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:96e56b4b170376860f623777977f7c72a19cd960854a48f568e1e92ca33f85e7","id":"relay.sipf-pension-payment-review.pension","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/pension-payment-review"],"required_scope":"registry:consult:sipf-pension-payment-review","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["payment_status"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json new file mode 100644 index 0000000..8426412 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"authorization":{"consent":{"required":false},"legal_basis":"social-insurance-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:ec6b5310716606dac48159abec1e512757fb9a18bff8a6e0ac634db932b2696b","id":"relay.sipf-survivor-benefit.survivor","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/survivor-benefit-determination"],"required_scope":"registry:consult:sipf-survivor-benefit","workload":"sipf-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"integration":{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["survivor_eligible"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":268435456,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":86400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json new file mode 100644 index 0000000..250ed74 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"payment_status":"active"},{"payment_status":"suspended"}],"headers":{},"status":200}}}],"name":"sipf-pension-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":true},"error":null,"outcome":"match","outputs":{"payment_status":"active"}},"input":{"uin":"2300109568"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"payment_status":"active"},"headers":{},"status":200}}}],"name":"sipf-pension-case-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"pension-payment-active":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-pension-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json new file mode 100644 index 0000000..27b99c4 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"solmara-sipf-pension-case-by-pensioner","operations":{},"outputs":{"payment_status":{"from":"snapshot.record.payment_status","max_bytes":128,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json new file mode 100644 index 0000000..c62fd58 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-pensioner/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-pensioner","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json new file mode 100644 index 0000000..11da61f --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{},"error":null,"outcome":"ambiguous","outputs":{}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[{"survivor_eligible":true},{"survivor_eligible":false}],"headers":{},"status":200}}}],"name":"sipf-survivor-case-ambiguous","variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":true},"error":null,"outcome":"match","outputs":{"survivor_eligible":true}},"input":{"uin":"2300118698"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"survivor_eligible":true},"headers":{},"status":200}}}],"name":"sipf-survivor-case-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"survivor-is-eligible":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"9999999999"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"sipf-survivor-case-no-match","variables":{}}],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json new file mode 100644 index 0000000..e246e22 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"solmara-sipf-pension-case-by-survivor","operations":{},"outputs":{"survivor_eligible":{"from":"snapshot.record.survivor_eligible","max_bytes":null,"maximum":null,"minimum":null,"nullable":false,"type":"boolean"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json new file mode 100644 index 0000000..72f6ab7 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/evidence/pension-case-by-survivor/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"solmara-sipf-pension-case-by-survivor","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json new file mode 100644 index 0000000..084f1aa --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-pensioner.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:835002ccbffb2a459808c4d2da2b7a631d7d6f0940c0aa73b0c7169cfb04299c"],"minimization":["sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660"],"negative_security":["sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d"]},"input_slots":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"solmara-sipf-pension-case-by-pensioner","output":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"payment_status":{"max_bytes":128,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json new file mode 100644 index 0000000..ed9366d --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/integration-packs/pension-case-by-survivor.json @@ -0,0 +1 @@ +{"id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:036a62f001ad4028a5c732f10cbea5c09bbb8c42fdd2bdc40675942bf7fd990f"],"minimization":["sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f"],"negative_security":["sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7"]},"input_slots":{"uin":{"maxLength":16,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":64}},"logical_operation":"solmara-sipf-pension-case-by-survivor","output":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":86400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"survivor_eligible":{"nullable":false,"type":"boolean"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json new file mode 100644 index 0000000..d40c606 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-pension-payment-review-pension.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:2bbd5327759f179a1a80324c5b134f43f6339781138547b3bb61ed8d06ffcbda","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-pensioner","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"pensioner_uin","physical_type":"utf8"}},"projection":{"payment_status":"payment_status"}},"table_provider":"pension__materialization_5b9c250beca9c627d5300e4bb7e831a5ff2ebc184c98142f4145e1bcdf0b42d7"},"profile":{"id":"solmara-sipf-pensions.sipf-pension-payment-review.pension","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_5b9c250beca9c627d5300e4bb7e831a5ff2ebc184c98142f4145e1bcdf0b42d7","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json new file mode 100644 index 0000000..5ecb41a --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:4b59cd1e953bcddf378da658fefd38bc2483ca92bd186907e7423880582dd9b3","id":"solmara-sipf-pensions.solmara-sipf-pension-case-by-survivor","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"spouse_uin","physical_type":"utf8"}},"projection":{"survivor_eligible":"survivor_eligible"}},"table_provider":"survivor__materialization_4469c4516b8b74da71d74914415251a07533406a6a6e1bbec963a302934d88d0"},"profile":{"id":"solmara-sipf-pensions.sipf-survivor-benefit.survivor","version":"1"},"registry_instance":"solmara-sipf-pensions","source_instance":"materialization_4469c4516b8b74da71d74914415251a07533406a6a6e1bbec963a302934d88d0","tenant":"solmara-sipf-pensions"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml b/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml new file mode 100644 index 0000000..e905e7b --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/config/relay.yaml @@ -0,0 +1,186 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - sipf-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://sipf-relay.solmara.registrystack.org + publisher: solmara-sipf-pensions + title: solmara-sipf-pensions governed Registry Relay +consultation: + artifacts: + evidence: + - class: conformance + path: artifacts/evidence/pension-case-by-survivor/conformance.json + sha256: sha256:036a62f001ad4028a5c732f10cbea5c09bbb8c42fdd2bdc40675942bf7fd990f + - class: minimization + path: artifacts/evidence/pension-case-by-survivor/minimization.json + sha256: sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f + - class: negative_security + path: artifacts/evidence/pension-case-by-survivor/negative-security.json + sha256: sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7 + - class: minimization + path: artifacts/evidence/pension-case-by-pensioner/minimization.json + sha256: sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660 + - class: conformance + path: artifacts/evidence/pension-case-by-pensioner/conformance.json + sha256: sha256:835002ccbffb2a459808c4d2da2b7a631d7d6f0940c0aa73b0c7169cfb04299c + - class: negative_security + path: artifacts/evidence/pension-case-by-pensioner/negative-security.json + sha256: sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d + integration_packs: + - hash: sha256:2bbd5327759f179a1a80324c5b134f43f6339781138547b3bb61ed8d06ffcbda + path: artifacts/integration-packs/pension-case-by-pensioner.json + sha256: sha256:c2991a8c90ecfaad79f28d60801f0a2772b0851d44a6ddc09fdcb1cc16498750 + - hash: sha256:4b59cd1e953bcddf378da658fefd38bc2483ca92bd186907e7423880582dd9b3 + path: artifacts/integration-packs/pension-case-by-survivor.json + sha256: sha256:0e5d17671c4bb70f2de35ebd92dc7fc745fa96539c89aaecb823014ea570b0c0 + private_bindings: + - hash: sha256:2afe86389a95b7cfc4c044cef4121eb76ef8d16e53062d23fc94e506147acd4a + path: artifacts/private-bindings/sipf-pension-payment-review-pension.json + sha256: sha256:a61426e41ec476090f81cad40af7c2cd841e78f656e95d0e59c28447772f2579 + - hash: sha256:ab18913ddee896c64080fb81c28753f17b5ceaf497e96d4f23b182ffe591ab4b + path: artifacts/private-bindings/sipf-survivor-benefit-survivor.json + sha256: sha256:e073578d218287585af339adda4092ff7e3545f52ac7000c515e38acad1c4791 + public_contracts: + - hash: sha256:47ee6aa3f5503173cd06fa791843849c421e818295dc74c4bbdbd7cdb2c753e4 + path: artifacts/consultation-contracts/sipf-pension-payment-review-pension.json + sha256: sha256:21eca329ac5c7d87291eff868247acd763a56a15920ce7a23a7b8b5dd29753ec + - hash: sha256:ede60666de002be1c1aae9279375c4b5e02171582c042b4d61ab6186a1e1c5d5 + path: artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json + sha256: sha256:718a57529960909aa4904412ae1010544352817a16db8bd3670a9f4baca4e52f + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: sipf-notary + principal_id: sipf-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 4444991830243562918 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 6117019989203477116 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized pension entity + entities: [] + id: pension + owner: solmara-sipf-pensions + sensitivity: personal + tables: + - access: + aggregate_scope: pension:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_5b9c250beca9c627d5300e4bb7e831a5ff2ebc184c98142f4145e1bcdf0b42d7 + materialization: snapshot + primary_key: pensioner_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: payment_status + nullable: false + sensitive: false + type: string + - name: pensioner_uin + nullable: false + sensitive: false + type: string + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_pension_payment + schema: public + type: postgres + title: pension + update_frequency: as_needed +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized survivor entity + entities: [] + id: survivor + owner: solmara-sipf-pensions + sensitivity: personal + tables: + - access: + aggregate_scope: survivor:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_4469c4516b8b74da71d74914415251a07533406a6a6e1bbec963a302934d88d0 + materialization: snapshot + primary_key: spouse_uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: spouse_uin + nullable: false + sensitive: false + type: string + - name: survivor_eligible + nullable: false + sensitive: false + type: boolean + strict: true + source: + connection_env: SOLMARA_SIPF_DATABASE_URL + table: + name: sipf_survivor_benefit + schema: public + type: postgres + title: survivor + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: sipf-pensions-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/sipf-pensions/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json new file mode 100644 index 0000000..db43ce7 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.json @@ -0,0 +1,65 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sipf-pensions", + "instance_id": "sipf-pensions-relay", + "bundle_id": "solmara-hosted-sipf-pensions-sequence-1", + "sequence": 1, + "config_hash": "sha256:17ffc50ba1e6e4e050fb6b384bb750253352a0189d000a477caf6e652e829daf", + "files": [ + { + "path": "config/artifacts/consultation-contracts/sipf-pension-payment-review-pension.json", + "sha256": "sha256:21eca329ac5c7d87291eff868247acd763a56a15920ce7a23a7b8b5dd29753ec" + }, + { + "path": "config/artifacts/consultation-contracts/sipf-survivor-benefit-survivor.json", + "sha256": "sha256:718a57529960909aa4904412ae1010544352817a16db8bd3670a9f4baca4e52f" + }, + { + "path": "config/artifacts/evidence/pension-case-by-pensioner/conformance.json", + "sha256": "sha256:835002ccbffb2a459808c4d2da2b7a631d7d6f0940c0aa73b0c7169cfb04299c" + }, + { + "path": "config/artifacts/evidence/pension-case-by-pensioner/minimization.json", + "sha256": "sha256:7955c481459333ab942755b224be6da87f180ea87b3e6bb35e1b56ddcdab5660" + }, + { + "path": "config/artifacts/evidence/pension-case-by-pensioner/negative-security.json", + "sha256": "sha256:dd7bf51e1b1a639d515b9aad9e3b598aa7b4f54760b191a8c363e28abd4e8a2d" + }, + { + "path": "config/artifacts/evidence/pension-case-by-survivor/conformance.json", + "sha256": "sha256:036a62f001ad4028a5c732f10cbea5c09bbb8c42fdd2bdc40675942bf7fd990f" + }, + { + "path": "config/artifacts/evidence/pension-case-by-survivor/minimization.json", + "sha256": "sha256:1a5c2d191a2468a4898556ef0e1cc164847f8df69e86a09c8f4c73eb1c705e5f" + }, + { + "path": "config/artifacts/evidence/pension-case-by-survivor/negative-security.json", + "sha256": "sha256:4ee198ac5719ce112ceb91f596e75dcbd933a8d6ceb0051910b876a3789147c7" + }, + { + "path": "config/artifacts/integration-packs/pension-case-by-pensioner.json", + "sha256": "sha256:c2991a8c90ecfaad79f28d60801f0a2772b0851d44a6ddc09fdcb1cc16498750" + }, + { + "path": "config/artifacts/integration-packs/pension-case-by-survivor.json", + "sha256": "sha256:0e5d17671c4bb70f2de35ebd92dc7fc745fa96539c89aaecb823014ea570b0c0" + }, + { + "path": "config/artifacts/private-bindings/sipf-pension-payment-review-pension.json", + "sha256": "sha256:a61426e41ec476090f81cad40af7c2cd841e78f656e95d0e59c28447772f2579" + }, + { + "path": "config/artifacts/private-bindings/sipf-survivor-benefit-survivor.json", + "sha256": "sha256:e073578d218287585af339adda4092ff7e3545f52ac7000c515e38acad1c4791" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:17ffc50ba1e6e4e050fb6b384bb750253352a0189d000a477caf6e652e829daf" + } + ], + "created_at": "2026-07-25T17:00:39.364434Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json new file mode 100644 index 0000000..ab8332d --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "cz6689XnrAFtp8iC6gcoGXJpFbUAYUKBPepLtmalfNO0nLWfKXuwRoWBVsWCllg4h6IDSd-pw8yMtIoYRn0wDw" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json b/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json new file mode 100644 index 0000000..9621235 --- /dev/null +++ b/config/hosted-relay-bundles/sipf-pensions/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sipf-pensions", + "instance_id": "sipf-pensions-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/antirollback-seed.json b/config/hosted-relay-bundles/sro-social/antirollback-seed.json new file mode 100644 index 0000000..d2748e6 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/antirollback-seed.json @@ -0,0 +1,9 @@ +{ + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sro-social" + }, + "last_sequence": 0, + "last_config_hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000" +} diff --git a/config/hosted-relay-bundles/sro-social/bootstrap.yaml b/config/hosted-relay-bundles/sro-social/bootstrap.yaml new file mode 100644 index 0000000..c0f70b1 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bootstrap.yaml @@ -0,0 +1,122 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - sro-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://sro-relay.solmara.registrystack.org + publisher: solmara-sro-social + title: solmara-sro-social governed Registry Relay +consultation: + artifacts: + evidence: + - class: negative_security + path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json + sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 + - class: minimization + path: artifacts/evidence/child-benefit-household-by-uin/minimization.json + sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 + - class: conformance + path: artifacts/evidence/child-benefit-household-by-uin/conformance.json + sha256: sha256:d89eed72dc7259ad5e645543668896ef781314bd8d56b3bcd36d3622688b5f24 + integration_packs: + - hash: sha256:45061b130799a47e6b4081f0bcb28bb2d9e9e9cdd73a0c1bc176c045425efb1c + path: artifacts/integration-packs/child-benefit-household-by-uin.json + sha256: sha256:37e318640dfa27a08d743cd4ff429a9af319c703660bebebb1e671eb1f8e1974 + private_bindings: + - hash: sha256:6edb16ffe8690c9037ee96cdddb57f19494cfa4945202e249b64566b3fa00555 + path: artifacts/private-bindings/child-benefit-household.json + sha256: sha256:907216b35efab81bb09f63b0e39725c941a42890e9a1c9ad5516560549a765ee + public_contracts: + - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 + path: artifacts/consultation-contracts/child-benefit-household.json + sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: sro-notary + principal_id: sro-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 368637434756268187 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 2694379094216255332 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized sro_child entity + entities: [] + id: sro_child + owner: solmara-sro-social + sensitivity: personal + tables: + - access: + aggregate_scope: sro_child:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: poverty_band + nullable: false + sensitive: false + type: string + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv + type: file + title: sro_child + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: sro-social-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/consultation-contracts/child-benefit-household.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/consultation-contracts/child-benefit-household.json new file mode 100644 index 0000000..e8bbb22 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/consultation-contracts/child-benefit-household.json @@ -0,0 +1 @@ +{"id":"solmara-sro-social.child-benefit.household","schema":"registry.relay.consultation-contract.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"authorization":{"consent":{"required":false},"legal_basis":"child-benefit-act","mandatory_obligations":[],"policy":{"decision_cache":"disabled","hash":"sha256:1f2bc3ace6f53ef9e6aa82974582fdd31113142f164f5afaf8328dafe39d92d3","id":"relay.child-benefit.household","max_decision_age_ms":1000,"unavailable":"deny"},"purposes":["https://id.registrystack.org/solmara/purpose/child-benefit-review"],"required_scope":"registry:consult:child-benefit","workload":"sro-notary"},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"inputs":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"integration":{"id":"solmara-sro-social.child-benefit-household-by-uin","revision":1},"materialization":{"digest_bound_active_pointer":true,"footprint":{"fields":["poverty_band"],"max_credential_exchanges":0,"max_data_destinations":1,"max_data_exchanges":1,"max_source_bytes":67108864,"max_source_records":1000000},"immutable_generation":true,"max_snapshot_age_ms":2678400000,"refresh_class":"scheduled","snapshot_retention_generations":2,"stale_behavior":"unavailable"},"output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"public_behavior":{"denial_code":"consultation.denied","denial_timing_profile":"measured-uniform-v1","outcomes":["match","no_match","ambiguous"]},"runtime":{"platform_profile":"registry-stack.consultation.v1","script_abi":null,"source_capability":"snapshot"},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}},"subject":{"mode":"single_subject","selector_provenance":{"type":"workload_selected"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json new file mode 100644 index 0000000..a158344 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/conformance.json @@ -0,0 +1 @@ +{"class":"conformance","fixtures":[{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":true},"error":null,"outcome":"match","outputs":{"poverty_band":"priority"}},"input":{"uin":"2300010248"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":{"poverty_band":"priority"},"headers":{},"status":200}}}],"name":"child-benefit-household-match","variables":{}},{"classification":"synthetic","expect":{"claims":{"household-below-poverty-threshold":false},"error":null,"outcome":"no_match","outputs":{}},"input":{"uin":"2300999997"},"interactions":[{"expect":{"body":null,"headers":{},"method":"GET","path":"/snapshot","query":{}},"respond":{"Http":{"body":[],"headers":{},"status":200}}}],"name":"child-benefit-household-no-match","variables":{}}],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json new file mode 100644 index 0000000..fc5378a --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/minimization.json @@ -0,0 +1 @@ +{"class":"minimization","integration":"child-benefit-household-by-uin","operations":{},"outputs":{"poverty_band":{"from":"snapshot.record.poverty_band","max_bytes":64,"maximum":null,"minimum":null,"nullable":false,"type":"string"}},"schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json new file mode 100644 index 0000000..9037e0a --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json @@ -0,0 +1 @@ +{"class":"negative_security","fixtures":[],"integration":"child-benefit-household-by-uin","schema":"registry.project.integration-evidence.v1"} diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json new file mode 100644 index 0000000..6916a6d --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/integration-packs/child-benefit-household-by-uin.json @@ -0,0 +1 @@ +{"id":"solmara-sro-social.child-benefit-household-by-uin","schema":"registry.relay.integration-pack.v1","spec":{"acquisition":{"class":"materialized_snapshot","fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}}},"bounds":{"max_credential_exchanges":0,"max_data_destinations":0,"max_data_exchanges":0,"max_disclosed_records":1,"max_in_flight":8,"max_source_bytes":1048576,"max_source_matches":2,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"deployment_parameters":{},"evidence":{"conformance":["sha256:d89eed72dc7259ad5e645543668896ef781314bd8d56b3bcd36d3622688b5f24"],"minimization":["sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0"],"negative_security":["sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95"]},"input_slots":{"uin":{"maxLength":10,"pattern":"^[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]$","role":"selector","type":"string","x-registry-canonicalization":"identity","x-registry-max-bytes":40}},"logical_operation":"child-benefit-household-by-uin","output":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"plan":{"credential_destination_slot":null,"credential_operation":null,"data_destination_slot":null,"kind":"snapshot_exact","rhai":null,"snapshot":{"immutable_generation":true,"max_snapshot_age_ms":2678400000,"unavailable":"unavailable"}},"reviewed_acquisition":{"cardinality":"probe_two","class":"materialized_snapshot","control_fields":{},"fields":{"poverty_band":{"max_bytes":64,"nullable":false,"type":"string"}},"reject_unknown_fields":true,"selector":{"components":{"uin":"snapshot_key"},"type":"snapshot_exact_and"}},"source_provenance":{"source_observed_at":{"type":"absent"},"source_revision":{"type":"absent"}}},"version":"1"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/private-bindings/child-benefit-household.json b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/private-bindings/child-benefit-household.json new file mode 100644 index 0000000..952ae7b --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/artifacts/private-bindings/child-benefit-household.json @@ -0,0 +1 @@ +{"capabilities":{"allow_script":false},"credential":null,"credential_destination":null,"data_destination":null,"deployment_parameters":{},"integration_pack":{"hash":"sha256:45061b130799a47e6b4081f0bcb28bb2d9e9e9cdd73a0c1bc176c045425efb1c","id":"solmara-sro-social.child-benefit-household-by-uin","version":"1"},"limits":{"max_in_flight":8,"max_public_response_bytes":65536,"max_source_bytes":1048576,"quota_burst":8,"quota_per_minute":60,"timeout_ms":15000},"materialization":{"mapping":{"keys":{"uin":{"comparison":"binary_equality","input":"uin","physical_field":"uin","physical_type":"utf8"}},"projection":{"poverty_band":"poverty_band"}},"table_provider":"sro_child__materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d"},"profile":{"id":"solmara-sro-social.child-benefit.household","version":"1"},"registry_instance":"solmara-sro-social","source_instance":"materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d","tenant":"solmara-sro-social"} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml b/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml new file mode 100644 index 0000000..c0f70b1 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/config/relay.yaml @@ -0,0 +1,122 @@ +audit: + hash_secret_env: REGISTRY_RELAY_AUDIT_HASH_SECRET + sink: stdout +auth: + mode: oidc + oidc: + allow_dev_insecure_fetch_urls: false + allowed_clients: + - sro-notary + audiences: + - registry-relay + issuer: https://workload-issuer.solmara.registrystack.org + jwks_url: https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json +catalog: + base_url: https://sro-relay.solmara.registrystack.org + publisher: solmara-sro-social + title: solmara-sro-social governed Registry Relay +consultation: + artifacts: + evidence: + - class: negative_security + path: artifacts/evidence/child-benefit-household-by-uin/negative-security.json + sha256: sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95 + - class: minimization + path: artifacts/evidence/child-benefit-household-by-uin/minimization.json + sha256: sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0 + - class: conformance + path: artifacts/evidence/child-benefit-household-by-uin/conformance.json + sha256: sha256:d89eed72dc7259ad5e645543668896ef781314bd8d56b3bcd36d3622688b5f24 + integration_packs: + - hash: sha256:45061b130799a47e6b4081f0bcb28bb2d9e9e9cdd73a0c1bc176c045425efb1c + path: artifacts/integration-packs/child-benefit-household-by-uin.json + sha256: sha256:37e318640dfa27a08d743cd4ff429a9af319c703660bebebb1e671eb1f8e1974 + private_bindings: + - hash: sha256:6edb16ffe8690c9037ee96cdddb57f19494cfa4945202e249b64566b3fa00555 + path: artifacts/private-bindings/child-benefit-household.json + sha256: sha256:907216b35efab81bb09f63b0e39725c941a42890e9a1c9ad5516560549a765ee + public_contracts: + - hash: sha256:7c6800ab1eea861c2e2500ba3e93bb9ddcb6d1212d7701756098155faaee4697 + path: artifacts/consultation-contracts/child-benefit-household.json + sha256: sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb + rhai_scripts: [] + audit_pseudonym_materials: + - key_id: epoch-1 + source: + name: REGISTRY_RELAY_AUDIT_PSEUDONYM_EPOCH_1 + provider: environment + authorized_workload: + audience: registry-relay + client_claim_selector: azp + client_value: sro-notary + principal_id: sro-notary + source_credentials: [] + state_plane: + audit_pseudonym_keyring_lock_key: 368637434756268187 + chain_key_epoch_id: project-consultation-chain-1 + database_url_env: REGISTRY_RELAY_CONSULTATION_DATABASE_URL + root_certificate_path: /etc/solmara/postgres/root.crt + serving_fence_lock_key: 2694379094216255332 +datasets: +- access_rights: restricted + aggregates: [] + conforms_to: [] + defaults: + materialization: snapshot + refresh: + interval: 1m + mode: interval + description: Materialized sro_child entity + entities: [] + id: sro_child + owner: solmara-sro-social + sensitivity: personal + tables: + - access: + aggregate_scope: sro_child:aggregate + metadata_scope: registry-internal:materialization + aggregates: [] + api: + allowed_filters: [] + default_limit: 1 + max_limit: 1 + require_purpose_header: false + id: materialization_a2fcdd986475cdde3629a2c14184c4d678c8f6a99ae777f97831bc4ffbcb540d + materialization: snapshot + primary_key: uin + refresh: + interval: 1m + mode: interval + schema: + fields: + - name: poverty_band + nullable: false + sensitive: false + type: string + - name: uin + nullable: false + sensitive: false + type: string + strict: true + source: + format: + csv: + delimiter: null + header_row: 1 + quote: null + path: /var/lib/solmara/ministries/social-development/fixtures/child_benefit_household.csv + type: file + title: sro_child + update_frequency: as_needed +deployment: + profile: hosted_lab +instance: + environment: hosted + id: sro-social-relay +server: + bind: 127.0.0.1:8080 +standards: {} +config_trust: + trust_anchor_path: /etc/solmara/hosted-relay-bundles/sro-social/trust-anchor.json + bundle_path: /etc/solmara/hosted-relay-bundles/sro-social/bundle + antirollback_state_path: /var/lib/registry-relay/cache/config-bundle-antirollback.json diff --git a/config/hosted-relay-bundles/sro-social/bundle/manifest.json b/config/hosted-relay-bundles/sro-social/bundle/manifest.json new file mode 100644 index 0000000..0821f8d --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/manifest.json @@ -0,0 +1,41 @@ +{ + "schema": "registry.platform.config_bundle.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sro-social", + "instance_id": "sro-social-relay", + "bundle_id": "solmara-hosted-sro-social-sequence-1", + "sequence": 1, + "config_hash": "sha256:c80e4b08837015b0189373c16f91c61af84ec884300fd53e12b5e58bfacd73d5", + "files": [ + { + "path": "config/artifacts/consultation-contracts/child-benefit-household.json", + "sha256": "sha256:77470bdaa56f3bf30beb1734e0aa0fd676068b09f45d576ce32cae4a51920efb" + }, + { + "path": "config/artifacts/evidence/child-benefit-household-by-uin/conformance.json", + "sha256": "sha256:d89eed72dc7259ad5e645543668896ef781314bd8d56b3bcd36d3622688b5f24" + }, + { + "path": "config/artifacts/evidence/child-benefit-household-by-uin/minimization.json", + "sha256": "sha256:7992e9674748854ae462ff87d054ab88e00aa5671c04de3b11c4863194fdbdd0" + }, + { + "path": "config/artifacts/evidence/child-benefit-household-by-uin/negative-security.json", + "sha256": "sha256:2862a52ca5baefa27db4330b1e8aaf5287c338be2e4733c10695528cd11c0b95" + }, + { + "path": "config/artifacts/integration-packs/child-benefit-household-by-uin.json", + "sha256": "sha256:37e318640dfa27a08d743cd4ff429a9af319c703660bebebb1e671eb1f8e1974" + }, + { + "path": "config/artifacts/private-bindings/child-benefit-household.json", + "sha256": "sha256:907216b35efab81bb09f63b0e39725c941a42890e9a1c9ad5516560549a765ee" + }, + { + "path": "config/relay.yaml", + "sha256": "sha256:c80e4b08837015b0189373c16f91c61af84ec884300fd53e12b5e58bfacd73d5" + } + ], + "created_at": "2026-07-25T17:00:39.283469Z" +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json b/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json new file mode 100644 index 0000000..085aa76 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/bundle/manifest.sig.json @@ -0,0 +1,10 @@ +{ + "schema": "registry.platform.config_bundle_signatures.v1", + "signatures": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "alg": "EdDSA", + "sig": "ZOfuPB8Ha4KaWnm3zoXou2WVgY4En67YUm9gcMPXV1DSzxjJRYYSbzvnPAF-iIL2i70JkPLyczRv9WXCJb43Cw" + } + ] +} \ No newline at end of file diff --git a/config/hosted-relay-bundles/sro-social/trust-anchor.json b/config/hosted-relay-bundles/sro-social/trust-anchor.json new file mode 100644 index 0000000..0d73ee2 --- /dev/null +++ b/config/hosted-relay-bundles/sro-social/trust-anchor.json @@ -0,0 +1,20 @@ +{ + "schema": "registry.platform.config_trust_anchor.v1", + "product": "registry-relay", + "environment": "hosted", + "stream_id": "solmara-hosted-sro-social", + "instance_id": "sro-social-relay", + "signers": [ + { + "kid": "0TiuAJUJc2oYr8DhDdqgsAnl_RSej7-731DWTOhdSu4", + "jwk": { + "kty": "OKP", + "kid": "solmara-hosted-relay-config-2026-07-25", + "alg": "EdDSA", + "crv": "Ed25519", + "x": "bmfusAem-Zz6IvSGa479gGdBMAovTEUPelPgGxwm7jg" + }, + "enabled": true + } + ] +} \ No newline at end of file diff --git a/docker/relay/Dockerfile b/docker/relay/Dockerfile index 709c470..1305827 100644 --- a/docker/relay/Dockerfile +++ b/docker/relay/Dockerfile @@ -1,11 +1,9 @@ ARG REGISTRY_RELAY_IMAGE=ghcr.io/registrystack/registry-relay@sha256:00681003d06667cde377e15640a3825eec97aca26f486cde2e9f5013ef56819e +ARG VOLUME_INIT_IMAGE=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 +FROM ${VOLUME_INIT_IMAGE} AS volume-init FROM ${REGISTRY_RELAY_IMAGE} +COPY --from=volume-init /bin/busybox /bin/busybox COPY ministries /var/lib/solmara/ministries -COPY runtime/registry-projects/hosted/cra-civil/relay /etc/solmara/registry-projects/hosted/cra-civil/relay -COPY runtime/registry-projects/hosted/nia-population/relay /etc/solmara/registry-projects/hosted/nia-population/relay -COPY runtime/registry-projects/hosted/sro-social/relay /etc/solmara/registry-projects/hosted/sro-social/relay -COPY runtime/registry-projects/hosted/mosd-programme/relay /etc/solmara/registry-projects/hosted/mosd-programme/relay -COPY runtime/registry-projects/hosted/sipf-pensions/relay /etc/solmara/registry-projects/hosted/sipf-pensions/relay -COPY runtime/registry-projects/hosted/nagdi-agriculture/relay /etc/solmara/registry-projects/hosted/nagdi-agriculture/relay +COPY config/hosted-relay-bundles /etc/solmara/hosted-relay-bundles COPY config/postgres/ssl/server.crt /etc/solmara/postgres/root.crt diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 3b302a7..30b8202 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -113,6 +113,25 @@ just registry-projects-sync just registry-projects-runtime-check ``` +Hosted Relay does not boot those compiler outputs as unsigned non-local +configuration. Each Relay wrapper carries an instance-bound signed Config +Bundle, its public trust anchor, and no private signing key. Verify that every +signed closure still projects exactly to the compiler output: + +```bash +just hosted-relay-bundles-check +``` + +When a hosted Relay config or artifact changes, use an offline private JWK and +its public-only JWK with +`scripts/generate-hosted-relay-bundles.py`. Increment the bundle sequence and +generate into a new staging directory, review the config and manifest diffs, +then replace the committed bundle set. Never commit the private JWK. A first +deployment seeds sequence zero only into an empty Relay cache volume; successful +Relay startup audits and persists the signed sequence. Later image rollback +cannot lower that durable sequence. Publish a higher signed sequence for a +normal rollback, or use Registry Relay's reviewed break-glass procedure. + Each authority application needs only the variables referenced by its Compose file. At minimum, provide: diff --git a/justfile b/justfile index 51b841e..6624a15 100644 --- a/justfile +++ b/justfile @@ -48,6 +48,10 @@ registry-projects-sync: registry-projects-runtime-check: scripts/registry-projects.sh check-runtime +# Verify signed hosted Relay closures against their public trust anchors. +hosted-relay-bundles-check: + uv run --locked scripts/check-hosted-relay-bundles.py + # Exercise one complete compiler-generated SRO blue-green release transition. contract-generation-proof: uv run --locked scripts/contract-generation-proof.py @@ -74,6 +78,7 @@ lint: scripts/check-fiction.sh scripts/check-image-pins.py scripts/check-config-secrets.py + just hosted-relay-bundles-check just metadata-publish-check just metadata-lint @if [ -f portal/package.json ]; then cd portal && pnpm check; fi diff --git a/scripts/check-config-secrets.py b/scripts/check-config-secrets.py index d481a24..1d9d819 100755 --- a/scripts/check-config-secrets.py +++ b/scripts/check-config-secrets.py @@ -13,6 +13,7 @@ "metadata", "projects", "runtime/registry-projects", + "config/hosted-relay-bundles", "compose.yaml", "compose.hosted.yaml", "compose.esignet.yaml", diff --git a/scripts/check-hosted-relay-bundles.py b/scripts/check-hosted-relay-bundles.py new file mode 100755 index 0000000..9a67c4b --- /dev/null +++ b/scripts/check-hosted-relay-bundles.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +"""Verify committed hosted Relay bundles and their generated source closure.""" + +from __future__ import annotations + +import copy +import json +import subprocess +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +PROJECTS = ( + "cra-civil", + "nia-population", + "sro-social", + "mosd-programme", + "sipf-pensions", + "nagdi-agriculture", +) +BUNDLE_ROOT = ROOT / "config" / "hosted-relay-bundles" +CONTAINER_ROOT = Path("/etc/solmara/hosted-relay-bundles") +ANTIROLLBACK_PATH = ( + "/var/lib/registry-relay/cache/config-bundle-antirollback.json" +) + + +def fail(message: str) -> None: + raise SystemExit(f"check-hosted-relay-bundles: {message}") + + +def registryctl_path() -> str: + result = subprocess.run( + [str(ROOT / "scripts" / "registryctl-pinned.sh"), "path"], + check=True, + stdout=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + + +def load_yaml(path: Path) -> dict[str, object]: + value = yaml.safe_load(path.read_text(encoding="utf-8")) + if not isinstance(value, dict): + fail(f"{path.relative_to(ROOT)} must contain a mapping") + return value + + +def main() -> int: + registryctl = registryctl_path() + for project in PROJECTS: + project_dir = BUNDLE_ROOT / project + bootstrap_path = project_dir / "bootstrap.yaml" + anchor_path = project_dir / "trust-anchor.json" + seed_path = project_dir / "antirollback-seed.json" + bundle_dir = project_dir / "bundle" + bundle_config_path = bundle_dir / "config" / "relay.yaml" + source_config_path = ( + ROOT + / "runtime" + / "registry-projects" + / "hosted" + / project + / "relay" + / "relay.yaml" + ) + for required in ( + bootstrap_path, + anchor_path, + seed_path, + bundle_dir / "manifest.json", + bundle_dir / "manifest.sig.json", + bundle_config_path, + source_config_path, + ): + if not required.is_file(): + fail(f"missing {required.relative_to(ROOT)}") + + bootstrap = load_yaml(bootstrap_path) + bundled = load_yaml(bundle_config_path) + source = load_yaml(source_config_path) + if bootstrap != bundled: + fail(f"{project} bootstrap config differs from signed config") + + container_dir = CONTAINER_ROOT / project + expected_trust = { + "trust_anchor_path": str(container_dir / "trust-anchor.json"), + "bundle_path": str(container_dir / "bundle"), + "antirollback_state_path": ANTIROLLBACK_PATH, + } + if bundled.get("config_trust") != expected_trust: + fail(f"{project} config trust paths are not deployment-bound") + unsigned_projection = copy.deepcopy(bundled) + unsigned_projection.pop("config_trust", None) + if unsigned_projection != source: + fail(f"{project} signed config differs from compiler output") + + anchor = json.loads(anchor_path.read_text(encoding="utf-8")) + manifest = json.loads( + (bundle_dir / "manifest.json").read_text(encoding="utf-8") + ) + instance_id = source["instance"]["id"] + stream_id = f"solmara-hosted-{project}" + expected_binding = { + "product": "registry-relay", + "environment": "hosted", + "stream_id": stream_id, + "instance_id": instance_id, + } + for key, expected in expected_binding.items(): + if anchor.get(key) != expected or manifest.get(key) != expected: + fail(f"{project} has an incorrect {key} binding") + if not isinstance(manifest.get("sequence"), int) or manifest["sequence"] < 1: + fail(f"{project} must use a positive bundle sequence") + if any("d" in signer.get("jwk", {}) for signer in anchor.get("signers", [])): + fail(f"{project} trust anchor contains private key material") + seed = json.loads(seed_path.read_text(encoding="utf-8")) + expected_seed = { + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": stream_id, + }, + "last_sequence": 0, + "last_config_hash": f"sha256:{'0' * 64}", + } + if seed != expected_seed: + fail(f"{project} anti-rollback seed is not the sequence-zero baseline") + + subprocess.run( + [ + registryctl, + "bundle", + "verify", + "--bundle-dir", + str(bundle_dir), + "--anchor-path", + str(anchor_path), + ], + check=True, + stdout=subprocess.DEVNULL, + ) + print("check-hosted-relay-bundles: ok") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/check-image-pins.py b/scripts/check-image-pins.py index 634736e..a7d8690 100755 --- a/scripts/check-image-pins.py +++ b/scripts/check-image-pins.py @@ -11,6 +11,10 @@ LINE_RE = re.compile(r"^([A-Z0-9_]+)=([^#\s]+)$") PIN_RE = re.compile(r"^[^#\s]+@sha256:[0-9a-f]{64}$") REGISTRY_STACK_IMAGE_KEYS = {"REGISTRY_RELAY_IMAGE", "REGISTRY_NOTARY_IMAGE"} +PINNED_IMAGE_KEYS = REGISTRY_STACK_IMAGE_KEYS | { + "SOLMARA_RELAY_RUNTIME_IMAGE", + "VOLUME_INIT_IMAGE", +} COMPOSE_FALLBACK_RE = re.compile( r"\$\{(?PREGISTRY_(?:RELAY|NOTARY)_IMAGE):-(?P[^}]+)\}" ) @@ -34,12 +38,12 @@ def main() -> int: continue key, value = match.groups() values[key] = value - if key in REGISTRY_STACK_IMAGE_KEYS and not PIN_RE.match(value): + if key in PINNED_IMAGE_KEYS and not PIN_RE.match(value): failures.append(f"versions.env:{line_no}: {key} must use image@sha256:<64 hex>") if "@latest" in value or ":latest" in value: failures.append(f"versions.env:{line_no}: latest tags are not allowed") - for key in REGISTRY_STACK_IMAGE_KEYS: + for key in PINNED_IMAGE_KEYS: if key not in values: failures.append(f"versions.env: {key} is required") diff --git a/scripts/generate-hosted-relay-bundles.py b/scripts/generate-hosted-relay-bundles.py new file mode 100755 index 0000000..0ab5dbd --- /dev/null +++ b/scripts/generate-hosted-relay-bundles.py @@ -0,0 +1,210 @@ +#!/usr/bin/env python3 +"""Generate signed, instance-bound Relay bundles for hosted Solmara.""" + +from __future__ import annotations + +import argparse +import json +import shutil +import subprocess +import tempfile +from pathlib import Path + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +PROJECTS = ( + "cra-civil", + "nia-population", + "sro-social", + "mosd-programme", + "sipf-pensions", + "nagdi-agriculture", +) +CONTAINER_ROOT = Path("/etc/solmara/hosted-relay-bundles") +ANTIROLLBACK_PATH = ( + "/var/lib/registry-relay/cache/config-bundle-antirollback.json" +) + + +def run(*args: str) -> None: + subprocess.run(args, check=True) + + +def registryctl_path() -> str: + result = subprocess.run( + [str(ROOT / "scripts" / "registryctl-pinned.sh"), "path"], + check=True, + stdout=subprocess.PIPE, + text=True, + ) + return result.stdout.strip() + + +def write_governed_config( + source: Path, + destination: Path, + project: str, +) -> dict[str, object]: + config = yaml.safe_load(source.read_text(encoding="utf-8")) + container_dir = CONTAINER_ROOT / project + config["config_trust"] = { + "trust_anchor_path": str(container_dir / "trust-anchor.json"), + "bundle_path": str(container_dir / "bundle"), + "antirollback_state_path": ANTIROLLBACK_PATH, + } + destination.write_text( + yaml.safe_dump(config, sort_keys=False), + encoding="utf-8", + ) + return config + + +def generate_project( + registryctl: str, + project: str, + private_jwk: Path, + public_jwk: Path, + sequence: int, + output_root: Path, +) -> None: + runtime_dir = ROOT / "runtime" / "registry-projects" / "hosted" / project / "relay" + source_config = runtime_dir / "relay.yaml" + if not source_config.is_file(): + raise SystemExit(f"missing hosted Relay config: {source_config}") + + instance_id = yaml.safe_load(source_config.read_text(encoding="utf-8"))[ + "instance" + ]["id"] + stream_id = f"solmara-hosted-{project}" + project_output = output_root / project + + with tempfile.TemporaryDirectory(prefix=f"solmara-{project}-bundle-") as temporary: + staging = Path(temporary) + input_config = staging / "input" / "config" + input_config.mkdir(parents=True) + governed_config = input_config / "relay.yaml" + write_governed_config(source_config, governed_config, project) + artifacts = runtime_dir / "artifacts" + if artifacts.exists(): + shutil.copytree(artifacts, input_config / "artifacts") + + bundle_dir = staging / "bundle" + run( + registryctl, + "bundle", + "sign", + "--input", + str(staging / "input"), + "--key", + str(private_jwk), + "--product", + "registry-relay", + "--environment", + "hosted", + "--stream-id", + stream_id, + "--instance-id", + instance_id, + "--sequence", + str(sequence), + "--bundle-id", + f"solmara-hosted-{project}-sequence-{sequence}", + "--out", + str(bundle_dir), + ) + + anchor = staging / "trust-anchor.json" + run( + registryctl, + "anchor", + "init", + "--anchor-path", + str(anchor), + "--product", + "registry-relay", + "--environment", + "hosted", + "--stream-id", + stream_id, + "--instance-id", + instance_id, + ) + run( + registryctl, + "anchor", + "add-key", + "--anchor-path", + str(anchor), + "--jwk-path", + str(public_jwk), + ) + run( + registryctl, + "bundle", + "verify", + "--bundle-dir", + str(bundle_dir), + "--anchor-path", + str(anchor), + ) + + project_output.mkdir(parents=True) + shutil.copy2(governed_config, project_output / "bootstrap.yaml") + shutil.copy2(anchor, project_output / "trust-anchor.json") + shutil.copytree(bundle_dir, project_output / "bundle") + seed = { + "key": { + "product": "registry-relay", + "environment": "hosted", + "stream_id": stream_id, + }, + "last_sequence": 0, + "last_config_hash": f"sha256:{'0' * 64}", + } + (project_output / "antirollback-seed.json").write_text( + json.dumps(seed, indent=2) + "\n", + encoding="utf-8", + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--private-jwk", type=Path, required=True) + parser.add_argument("--public-jwk", type=Path, required=True) + parser.add_argument("--sequence", type=int, default=1) + parser.add_argument("--out", type=Path, required=True) + args = parser.parse_args() + + if args.sequence < 1: + raise SystemExit("--sequence must be positive") + for key_path in (args.private_jwk, args.public_jwk): + if not key_path.is_file(): + raise SystemExit(f"missing signing key: {key_path}") + private_jwk = json.loads(args.private_jwk.read_text(encoding="utf-8")) + public_jwk = json.loads(args.public_jwk.read_text(encoding="utf-8")) + if "d" not in private_jwk or "d" in public_jwk: + raise SystemExit("expected a private JWK and a public-only JWK") + if args.out.exists(): + raise SystemExit(f"output path must not exist: {args.out}") + + registryctl = registryctl_path() + args.out.mkdir(parents=True) + for project in PROJECTS: + generate_project( + registryctl, + project, + args.private_jwk, + args.public_jwk, + args.sequence, + args.out, + ) + for path in args.out.rglob("*"): + path.chmod(0o755 if path.is_dir() else 0o644) + print(f"Wrote {args.out}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index cd2c1f7..8499b47 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -771,6 +771,10 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: r"@sha256:[0-9a-f]{64}$" ), ) + self.assertRegex( + versions["VOLUME_INIT_IMAGE"], + r"^busybox@sha256:[0-9a-f]{64}$", + ) self.assertEqual( versions["SOLMARA_RELAY_RUNTIME_DEV_IMAGE"], "solmara-lab-registry-relay:v0.13.0-attribute-release", @@ -834,10 +838,22 @@ def test_relay_runtime_opts_into_only_the_required_beta_feature(self) -> None: "REGISTRY_RELAY_IMAGE=${{ env.SOLMARA_RELAY_RUNTIME_IMAGE }}", release_workflow, ) + self.assertIn( + "VOLUME_INIT_IMAGE=${{ env.VOLUME_INIT_IMAGE }}", + release_workflow, + ) self.assertIn( 'echo "- ${SOLMARA_RELAY_RUNTIME_IMAGE}"', release_workflow, ) + self.assertLess( + release_workflow.index("- name: Build and push hosted Relay image"), + release_workflow.index("- name: Verify hosted Relay signed bundles"), + ) + self.assertIn( + "config verify-bundle", + release_workflow, + ) def test_coolify_authority_state_is_postgresql_isolated(self) -> None: authority_groups = { @@ -920,10 +936,17 @@ def test_coolify_authority_state_is_postgresql_isolated(self) -> None: notary = services[notary_name] installer = services[f"{notary_name}-state-install"] bootstrap = services[f"{key}-relay-state-bootstrap"] + config_state_init = services[ + f"{key}-relay-config-state-init" + ] relay_mounts = set(relay.get("volumes") or []) + bootstrap_mounts = set(bootstrap.get("volumes") or []) + config_state_mounts = set( + config_state_init.get("volumes") or [] + ) notary_mounts = set(notary.get("volumes") or []) relay_config = ( - f"/etc/solmara/registry-projects/hosted/{project}/relay/relay.yaml" + f"/etc/solmara/hosted-relay-bundles/{project}/bootstrap.yaml" ) notary_config = ( f"/etc/solmara/registry-projects/hosted/{project}/notary/notary.yaml" @@ -940,6 +963,25 @@ def test_coolify_authority_state_is_postgresql_isolated(self) -> None: f"{key}-relay-cache:/var/lib/registry-relay/cache", relay_mounts, ) + self.assertIn( + f"{key}-relay-cache:/var/lib/registry-relay/cache", + bootstrap_mounts, + ) + self.assertEqual(config_state_init["user"], "0:0") + self.assertEqual( + config_state_init["entrypoint"], + ["/bin/busybox", "sh", "-eu", "-c"], + ) + self.assertIn( + f"{key}-relay-cache:/var/lib/registry-relay/cache", + config_state_mounts, + ) + self.assertEqual( + bootstrap["depends_on"][ + f"{key}-relay-config-state-init" + ]["condition"], + "service_completed_successfully", + ) self.assertEqual( notary["command"], ["--config", notary_config] ) @@ -996,16 +1038,24 @@ def test_hosted_authority_images_contain_runtime_closures_and_tls(self) -> None: ) for project in projects: with self.subTest(project=project): - self.assertIn( - f"COPY runtime/registry-projects/hosted/{project}/relay " - f"/etc/solmara/registry-projects/hosted/{project}/relay", - relay_dockerfile, - ) self.assertIn( f"COPY runtime/registry-projects/hosted/{project}/notary/notary.yaml " f"/etc/solmara/registry-projects/hosted/{project}/notary/notary.yaml", notary_dockerfile, ) + self.assertIn( + "COPY config/hosted-relay-bundles " + "/etc/solmara/hosted-relay-bundles", + relay_dockerfile, + ) + self.assertIn( + "COPY --from=volume-init /bin/busybox /bin/busybox", + relay_dockerfile, + ) + self.assertNotIn( + "COPY runtime/registry-projects/hosted/", + relay_dockerfile, + ) tls_copy = ( "COPY config/postgres/ssl/server.crt /etc/solmara/postgres/root.crt" ) diff --git a/versions.env b/versions.env index 5422316..2b9f83d 100644 --- a/versions.env +++ b/versions.env @@ -9,5 +9,5 @@ SOLMARA_RELAY_RUNTIME_DEV_IMAGE=solmara-lab-registry-relay:v0.13.0-attribute-rel REGISTRY_RELAY_STATE_EPOCH=v013 POSTGRES_IMAGE=postgres:16-alpine STATIC_METADATA_IMAGE=python:3.12-alpine -VOLUME_INIT_IMAGE=busybox:1.36 +VOLUME_INIT_IMAGE=busybox@sha256:73aaf090f3d85aa34ee199857f03fa3a95c8ede2ffd4cc2cdb5b94e566b11662 SOLMARA_PORTAL_IMAGE=solmara-lab-portal:local From abfda66651ae30a1455fa53c4e223e47c0d06a04 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 00:14:29 +0700 Subject: [PATCH 10/14] Update auxiliary image pin fixture Signed-off-by: Jeremi Joslin --- scripts/test_image_pins.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/test_image_pins.py b/scripts/test_image_pins.py index fe409ea..6d30d06 100644 --- a/scripts/test_image_pins.py +++ b/scripts/test_image_pins.py @@ -12,6 +12,8 @@ ROOT = Path(__file__).resolve().parents[1] RELAY = "ghcr.io/registrystack/registry-relay@sha256:" + "1" * 64 NOTARY = "ghcr.io/registrystack/registry-notary@sha256:" + "2" * 64 +RELAY_RUNTIME = "ghcr.io/registrystack/solmara-lab-relay-runtime@sha256:" + "3" * 64 +VOLUME_INIT = "busybox@sha256:" + "4" * 64 def load_check_image_pins(): @@ -33,7 +35,10 @@ def setUp(self) -> None: self.root = Path(self.directory.name) self.module.ROOT = self.root (self.root / "versions.env").write_text( - f"REGISTRY_RELAY_IMAGE={RELAY}\nREGISTRY_NOTARY_IMAGE={NOTARY}\n", + f"REGISTRY_RELAY_IMAGE={RELAY}\n" + f"REGISTRY_NOTARY_IMAGE={NOTARY}\n" + f"SOLMARA_RELAY_RUNTIME_IMAGE={RELAY_RUNTIME}\n" + f"VOLUME_INIT_IMAGE={VOLUME_INIT}\n", encoding="utf-8", ) From 87d8ead8521a5df4d89504b9ee1581a48ad6486e Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 00:42:32 +0700 Subject: [PATCH 11/14] Host Relay workload identity for deployed apps Signed-off-by: Jeremi Joslin --- README.md | 4 + compose.coolify.agriculture.yaml | 28 ++++- compose.coolify.esignet.yaml | 21 +++- compose.coolify.interior.yaml | 44 ++++++- compose.coolify.labour-pensions.yaml | 28 ++++- compose.coolify.social-development.yaml | 44 ++++++- compose.coolify.yaml | 11 ++ docs/hosted-deployment.md | 53 ++++----- metadata/public/.well-known/jwks.json | 53 +++++++++ scripts/check-hosted-relay-bundles.py | 33 ++++++ scripts/relay_workload_identity_agent.py | 50 ++++++-- scripts/test_hosted_relay_bundles.py | 63 ++++++++++ scripts/test_quality_scripts.py | 111 ++++++++++++++++-- scripts/test_relay_workload_identity_agent.py | 28 ++++- 14 files changed, 497 insertions(+), 74 deletions(-) create mode 100644 metadata/public/.well-known/jwks.json create mode 100644 scripts/test_hosted_relay_bundles.py diff --git a/README.md b/README.md index 5e8e23d..bed8028 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,10 @@ 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. +Hosted workload agents keep Relay bearer credentials short-lived and confined +to per-consumer volumes. The separately served workload JWKS contains public +keys only; private workload JWKs remain Coolify secrets. + 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. diff --git a/compose.coolify.agriculture.yaml b/compose.coolify.agriculture.yaml index fc511d6..09a6f57 100644 --- a/compose.coolify.agriculture.yaml +++ b/compose.coolify.agriculture.yaml @@ -32,6 +32,16 @@ x-relay-config-state-init: &relay-config-state-init restart: "no" entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] +x-workload-agent-common: &workload-agent-common + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + command: ["python", "scripts/relay_workload_identity_agent.py"] + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 6 + x-notary-common: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -90,6 +100,16 @@ services: postgres: condition: service_healthy + nagdi-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"nagdi-notary","subject":"nagdi-notary","scopes":["registry:consult:livestock","registry:consult:voucher"],"token_file":"/run/secrets/nagdi-notary-relay-token","private_jwk_env":"NAGDI_RELAY_WORKLOAD_JWK"}] + NAGDI_RELAY_WORKLOAD_JWK: ${NAGDI_RELAY_WORKLOAD_JWK} + volumes: + - nagdi-workload-token:/run/secrets + nagdi-relay-config-state-init: <<: *relay-config-state-init command: @@ -157,6 +177,8 @@ services: condition: service_healthy nagdi-notary-state-install: condition: service_completed_successfully + nagdi-workload-agent: + condition: service_healthy labels: solmara.lab.host: nagdi-notary.solmara.registrystack.org @@ -171,14 +193,12 @@ services: volumes: - nagdi-workload-token:/run/secrets:ro depends_on: + nagdi-workload-agent: + condition: service_healthy registry-postgresql-bootstrap: condition: service_completed_successfully volumes: postgres-data: nagdi-relay-cache: - # The hosted workload issuer rotates the short-lived Notary token into this - # authority volume. Runtime and migration containers mount it read-only. nagdi-workload-token: - external: true - name: ${NAGDI_WORKLOAD_TOKEN_VOLUME:-solmara-nagdi-workload-token} diff --git a/compose.coolify.esignet.yaml b/compose.coolify.esignet.yaml index ce6aed4..ac5a630 100644 --- a/compose.coolify.esignet.yaml +++ b/compose.coolify.esignet.yaml @@ -26,6 +26,23 @@ services: timeout: 5s retries: 30 + nia-esignet-workload-agent: + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + command: ["python", "scripts/relay_workload_identity_agent.py"] + restart: unless-stopped + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"solmara-esignet","subject":"solmara-esignet","scopes":["population:identity_release"],"token_file":"/run/secrets/solmara-esignet-relay-token","private_jwk_env":"NIA_ESIGNET_RELAY_WORKLOAD_JWK","token_uid":1001,"token_gid":1001}] + NIA_ESIGNET_RELAY_WORKLOAD_JWK: ${NIA_ESIGNET_RELAY_WORKLOAD_JWK} + volumes: + - nia-esignet-workload-token:/run/secrets + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 6 + esignet: image: ${SOLMARA_ESIGNET_RELAY_IMAGE:?set digest-pinned Solmara eSignet relay image} user: root @@ -90,6 +107,8 @@ services: condition: service_healthy esignet-redis: condition: service_healthy + nia-esignet-workload-agent: + condition: service_healthy healthcheck: test: ["CMD", "curl", "-fsS", "http://esignet:8088/v1/esignet/oidc/.well-known/openid-configuration"] interval: 10s @@ -182,5 +201,3 @@ volumes: esignet-kyc-signing: esignet-redis-data: nia-esignet-workload-token: - external: true - name: ${NIA_ESIGNET_WORKLOAD_TOKEN_VOLUME:-solmara-nia-esignet-workload-token} diff --git a/compose.coolify.interior.yaml b/compose.coolify.interior.yaml index 1ea2e39..ed24e00 100644 --- a/compose.coolify.interior.yaml +++ b/compose.coolify.interior.yaml @@ -32,6 +32,16 @@ x-relay-config-state-init: &relay-config-state-init restart: "no" entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] +x-workload-agent-common: &workload-agent-common + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + command: ["python", "scripts/relay_workload_identity_agent.py"] + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 6 + x-notary-common: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -96,6 +106,26 @@ services: postgres: condition: service_healthy + cra-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"cra-notary","subject":"cra-notary","scopes":["registry:consult:cra-child-benefit","registry:consult:cra-citizen-record","registry:consult:cra-pension-death"],"token_file":"/run/secrets/cra-notary-relay-token","private_jwk_env":"CRA_RELAY_WORKLOAD_JWK"}] + CRA_RELAY_WORKLOAD_JWK: ${CRA_RELAY_WORKLOAD_JWK} + volumes: + - cra-workload-token:/run/secrets + + nia-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"nia-notary","subject":"nia-notary","scopes":["registry:consult:nia-child-benefit","registry:consult:nia-citizen-status"],"token_file":"/run/secrets/nia-notary-relay-token","private_jwk_env":"NIA_RELAY_WORKLOAD_JWK"}] + NIA_RELAY_WORKLOAD_JWK: ${NIA_RELAY_WORKLOAD_JWK} + volumes: + - nia-workload-token:/run/secrets + cra-relay-config-state-init: <<: *relay-config-state-init command: @@ -217,6 +247,8 @@ services: condition: service_healthy cra-notary-state-install: condition: service_completed_successfully + cra-workload-agent: + condition: service_healthy labels: solmara.lab.host: cra-notary.solmara.registrystack.org @@ -231,6 +263,8 @@ services: volumes: - cra-workload-token:/run/secrets:ro depends_on: + cra-workload-agent: + condition: service_healthy registry-postgresql-bootstrap: condition: service_completed_successfully @@ -252,6 +286,8 @@ services: condition: service_healthy nia-notary-state-install: condition: service_completed_successfully + nia-workload-agent: + condition: service_healthy labels: solmara.lab.host: nia-notary.solmara.registrystack.org @@ -266,6 +302,8 @@ services: volumes: - nia-workload-token:/run/secrets:ro depends_on: + nia-workload-agent: + condition: service_healthy registry-postgresql-bootstrap: condition: service_completed_successfully @@ -273,11 +311,5 @@ volumes: postgres-data: cra-relay-cache: nia-relay-cache: - # The hosted workload issuer rotates each short-lived Notary token into its - # authority volume. Runtime and migration containers mount the volume read-only. cra-workload-token: - external: true - name: ${CRA_WORKLOAD_TOKEN_VOLUME:-solmara-cra-workload-token} nia-workload-token: - external: true - name: ${NIA_WORKLOAD_TOKEN_VOLUME:-solmara-nia-workload-token} diff --git a/compose.coolify.labour-pensions.yaml b/compose.coolify.labour-pensions.yaml index 71a2714..8ac891f 100644 --- a/compose.coolify.labour-pensions.yaml +++ b/compose.coolify.labour-pensions.yaml @@ -32,6 +32,16 @@ x-relay-config-state-init: &relay-config-state-init restart: "no" entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] +x-workload-agent-common: &workload-agent-common + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + command: ["python", "scripts/relay_workload_identity_agent.py"] + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 6 + x-notary-common: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -91,6 +101,16 @@ services: postgres: condition: service_healthy + sipf-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"sipf-notary","subject":"sipf-notary","scopes":["registry:consult:sipf-pension-payment-review","registry:consult:sipf-survivor-benefit"],"token_file":"/run/secrets/sipf-notary-relay-token","private_jwk_env":"SIPF_RELAY_WORKLOAD_JWK"}] + SIPF_RELAY_WORKLOAD_JWK: ${SIPF_RELAY_WORKLOAD_JWK} + volumes: + - sipf-workload-token:/run/secrets + sipf-relay-config-state-init: <<: *relay-config-state-init command: @@ -161,6 +181,8 @@ services: condition: service_healthy sipf-notary-state-install: condition: service_completed_successfully + sipf-workload-agent: + condition: service_healthy labels: solmara.lab.host: sipf-notary.solmara.registrystack.org @@ -177,12 +199,10 @@ services: depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully + sipf-workload-agent: + condition: service_healthy volumes: postgres-data: sipf-relay-cache: - # The hosted workload issuer rotates the short-lived Notary token into this - # authority volume. Runtime and migration containers mount it read-only. sipf-workload-token: - external: true - name: ${SIPF_WORKLOAD_TOKEN_VOLUME:-solmara-sipf-workload-token} diff --git a/compose.coolify.social-development.yaml b/compose.coolify.social-development.yaml index 3342419..d912f46 100644 --- a/compose.coolify.social-development.yaml +++ b/compose.coolify.social-development.yaml @@ -32,6 +32,16 @@ x-relay-config-state-init: &relay-config-state-init restart: "no" entrypoint: ["/bin/busybox", "sh", "-eu", "-c"] +x-workload-agent-common: &workload-agent-common + image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} + command: ["python", "scripts/relay_workload_identity_agent.py"] + restart: unless-stopped + healthcheck: + test: ["CMD-SHELL", "python -c \"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8090/health', timeout=3)\""] + interval: 10s + timeout: 5s + retries: 6 + x-notary-common: ¬ary-common image: ${SOLMARA_NOTARY_IMAGE:?set digest-pinned Solmara notary image} platform: ${REGISTRY_STACK_PLATFORM:-linux/amd64} @@ -95,6 +105,26 @@ services: postgres: condition: service_healthy + sro-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"sro-notary","subject":"sro-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/sro-notary-relay-token","private_jwk_env":"SRO_RELAY_WORKLOAD_JWK"}] + SRO_RELAY_WORKLOAD_JWK: ${SRO_RELAY_WORKLOAD_JWK} + volumes: + - sro-workload-token:/run/secrets + + programme-workload-agent: + <<: *workload-agent-common + environment: + WORKLOAD_ISSUER: https://workload-issuer.solmara.registrystack.org + WORKLOAD_IDENTITIES_JSON: >- + [{"audience":"registry-relay","azp":"programme-notary","subject":"programme-notary","scopes":["registry:consult:child-benefit"],"token_file":"/run/secrets/programme-notary-relay-token","private_jwk_env":"PROGRAMME_RELAY_WORKLOAD_JWK"}] + PROGRAMME_RELAY_WORKLOAD_JWK: ${PROGRAMME_RELAY_WORKLOAD_JWK} + volumes: + - programme-workload-token:/run/secrets + sro-relay-config-state-init: <<: *relay-config-state-init command: @@ -211,6 +241,8 @@ services: condition: service_healthy sro-notary-state-install: condition: service_completed_successfully + sro-workload-agent: + condition: service_healthy labels: solmara.lab.host: sro-notary.solmara.registrystack.org @@ -227,6 +259,8 @@ services: depends_on: registry-postgresql-bootstrap: condition: service_completed_successfully + sro-workload-agent: + condition: service_healthy programme-notary: <<: *notary-common @@ -244,6 +278,8 @@ services: condition: service_healthy programme-notary-state-install: condition: service_completed_successfully + programme-workload-agent: + condition: service_healthy labels: solmara.lab.host: programme-notary.solmara.registrystack.org @@ -258,6 +294,8 @@ services: volumes: - programme-workload-token:/run/secrets:ro depends_on: + programme-workload-agent: + condition: service_healthy registry-postgresql-bootstrap: condition: service_completed_successfully @@ -265,11 +303,5 @@ volumes: postgres-data: sro-relay-cache: programme-relay-cache: - # The hosted workload issuer rotates each short-lived Notary token into its - # authority volume. Runtime and migration containers mount the volume read-only. sro-workload-token: - external: true - name: ${SRO_WORKLOAD_TOKEN_VOLUME:-solmara-sro-workload-token} programme-workload-token: - external: true - name: ${PROGRAMME_WORKLOAD_TOKEN_VOLUME:-solmara-programme-workload-token} diff --git a/compose.coolify.yaml b/compose.coolify.yaml index 2c9c857..6b712eb 100644 --- a/compose.coolify.yaml +++ b/compose.coolify.yaml @@ -11,6 +11,17 @@ services: labels: solmara.lab.host: metadata.solmara.registrystack.org + workload-issuer: + image: ${SOLMARA_STATIC_METADATA_IMAGE:?set digest-pinned Solmara static metadata image} + command: ["python", "-m", "http.server", "8080", "--bind", "0.0.0.0", "--directory", "/srv/static"] + healthcheck: + test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/.well-known/jwks.json"] + interval: 30s + timeout: 10s + retries: 3 + labels: + solmara.lab.host: workload-issuer.solmara.registrystack.org + scenario-runner: image: ${SOLMARA_SCENARIO_RUNNER_IMAGE:?set digest-pinned Solmara scenario runner image} environment: diff --git a/docs/hosted-deployment.md b/docs/hosted-deployment.md index 30b8202..719aa8a 100644 --- a/docs/hosted-deployment.md +++ b/docs/hosted-deployment.md @@ -163,40 +163,27 @@ include secret values when the caller has sensitive read access. ## Workload identity prerequisite -Each hosted authority Notary reads a short-lived Relay workload token from a -read-only external volume. The authority Compose files declare these volumes -with names such as `solmara-cra-workload-token`; they do not mint long-lived -tokens at container startup. Every token uses audience `registry-relay`, and -its `azp`, `sub`, scopes, key, and output file belong to one workload identity. - -Before deploying an authority application: - -1. Create its external workload-token volume. -2. Configure the hosted workload issuer for audience `registry-relay` and the - matching authority Notary identity. -3. Have the issuer rotate the token file into that volume. -4. Confirm only the Notary runtime and state installer mount the volume, both - read-only. - -A missing or expired token keeps the Notary unready. Do not replace this flow -with a static API token in Compose. - -The NIA issuer has one additional, separately keyed identity for eSignet. Use +Each hosted authority application runs a loopback-only workload agent that +rotates a five-minute Relay token into its authority-owned named volume. The +Notary runtime and state installer mount that volume read-only and wait for the +agent to become healthy. Private workload JWKs enter only as Coolify production +secrets; they are not present in images, public metadata, or Compose defaults. +A missing, malformed, or expired token keeps the dependent workload unready. + +The public half of every hosted workload key is committed at +`metadata/public/.well-known/jwks.json` and served separately at +`https://workload-issuer.solmara.registrystack.org/.well-known/jwks.json`. +Relay tokens use that HTTPS origin as `iss`, while the token agents remain +bound to container loopback. Keep the public JWKS and all corresponding +Coolify private JWKs in one reviewed rotation. + +The eSignet application has a separate NIA identity with `azp=solmara-esignet`, `sub=solmara-esignet`, and exactly the -`population:identity_release` scope. Publish that key alongside the NIA Notary -key at the NIA issuer's JWKS endpoint, and rotate the token as -`solmara-esignet-relay-token`, owned by UID/GID `1001:1001`, into the external -`solmara-nia-esignet-workload-token` volume. Only the hosted workload issuer -and eSignet may mount this volume. The NIA Notary and its state installer must -continue to mount only `solmara-nia-workload-token`, so neither can read the -eSignet credential. - -Create and populate the eSignet-only external volume before deploying -`compose.coolify.esignet.yaml`. The eSignet plugin rereads the token file for -each Relay request, so issuer rotation does not require an eSignet restart. An -unreadable, empty, malformed, or expired file fails the Relay-backed -authentication path closed. Do not copy the token into a Coolify environment -variable or reuse the NIA Notary identity. +`population:identity_release` scope. Its agent writes +`solmara-esignet-relay-token` as UID/GID `1001:1001` into an eSignet-owned +volume. The eSignet plugin rereads that file for each Relay request, so normal +rotation does not require an eSignet restart. Do not copy the token itself into +a Coolify environment variable or reuse the NIA Notary identity. ## Coolify application setup diff --git a/metadata/public/.well-known/jwks.json b/metadata/public/.well-known/jwks.json new file mode 100644 index 0000000..3c0658a --- /dev/null +++ b/metadata/public/.well-known/jwks.json @@ -0,0 +1,53 @@ +{ + "keys": [ + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-cra-relay-workload-key-1", + "kty": "OKP", + "x": "UazABMCeaN2hgksLJmeJfdk8bZDT2gRweCno-LEKK3g" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-nagdi-relay-workload-key-1", + "kty": "OKP", + "x": "zAgQ4qvveNcLrj-YGFKzMVp-KTQL47RFkHr0x1xR0e4" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-nia-esignet-relay-workload-key-1", + "kty": "OKP", + "x": "isAltfG5GvqFX5DZmj9NNFfiNDJVvhNtHpyQtQIWI-o" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-nia-relay-workload-key-1", + "kty": "OKP", + "x": "GxzXobbS9FQp73ZpKeIDP2wDSwM9k8JzGTvO3pJy64M" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-programme-relay-workload-key-1", + "kty": "OKP", + "x": "-pveKSRIMRjmeEBGp9Fa5nzCAdre0uiqL2Ryxczf8uY" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-sipf-relay-workload-key-1", + "kty": "OKP", + "x": "cKQaa0lD6RxXIpYUpF5RAQgD0g2MRCo2ere_UwHgrK8" + }, + { + "alg": "EdDSA", + "crv": "Ed25519", + "kid": "solmara-sro-relay-workload-key-1", + "kty": "OKP", + "x": "GzGnvI5XJfSqo9skMrFfv7cdK_nqjy-qD-AkFNy87rM" + } + ] +} diff --git a/scripts/check-hosted-relay-bundles.py b/scripts/check-hosted-relay-bundles.py index 9a67c4b..900f220 100755 --- a/scripts/check-hosted-relay-bundles.py +++ b/scripts/check-hosted-relay-bundles.py @@ -48,6 +48,36 @@ def load_yaml(path: Path) -> dict[str, object]: return value +def regular_file_tree(root: Path) -> dict[str, bytes]: + if not root.is_dir(): + fail(f"missing artifact directory {root}") + files: dict[str, bytes] = {} + for path in sorted(root.rglob("*")): + relative = path.relative_to(root).as_posix() + if path.is_symlink(): + fail(f"artifact tree contains symlink {relative}") + if path.is_dir(): + continue + if not path.is_file(): + fail(f"artifact tree contains non-file {relative}") + files[relative] = path.read_bytes() + return files + + +def verify_artifact_closure( + project: str, bundled_artifacts: Path, source_artifacts: Path +) -> None: + bundled_files = regular_file_tree(bundled_artifacts) + source_files = regular_file_tree(source_artifacts) + if bundled_files.keys() != source_files.keys(): + fail(f"{project} signed artifact paths differ from compiler output") + for relative, bundled_content in bundled_files.items(): + if bundled_content != source_files[relative]: + fail( + f"{project} signed artifact {relative} differs from compiler output" + ) + + def main() -> int: registryctl = registryctl_path() for project in PROJECTS: @@ -66,6 +96,8 @@ def main() -> int: / "relay" / "relay.yaml" ) + bundled_artifacts = bundle_dir / "config" / "artifacts" + source_artifacts = source_config_path.parent / "artifacts" for required in ( bootstrap_path, anchor_path, @@ -96,6 +128,7 @@ def main() -> int: unsigned_projection.pop("config_trust", None) if unsigned_projection != source: fail(f"{project} signed config differs from compiler output") + verify_artifact_closure(project, bundled_artifacts, source_artifacts) anchor = json.loads(anchor_path.read_text(encoding="utf-8")) manifest = json.loads( diff --git a/scripts/relay_workload_identity_agent.py b/scripts/relay_workload_identity_agent.py index 4095956..a6b7174 100755 --- a/scripts/relay_workload_identity_agent.py +++ b/scripts/relay_workload_identity_agent.py @@ -3,7 +3,9 @@ The agent binds only to IPv4 loopback. It publishes the public half of every configured Ed25519 JWK from one endpoint and atomically maintains one access -token file per identity. Private JWK values are read indirectly: each bounded +token file per identity. Tokens may identify either the local loopback issuer +or a separately published HTTPS issuer whose public-only JWKS contains the same +keys. Private JWK values are read indirectly: each bounded WORKLOAD_IDENTITIES_JSON entry names the environment variable containing its JWK JSON. """ @@ -13,6 +15,7 @@ import base64 import binascii import hashlib +import ipaddress import json import os import re @@ -58,6 +61,10 @@ ENV_NAME_PATTERN = re.compile(r"[A-Z][A-Z0-9_]{0,127}\Z") TOKEN_VALUE_PATTERN = re.compile(r"[\x21-\x7e]+\Z") BASE64URL_PATTERN = re.compile(r"[A-Za-z0-9_-]+\Z") +DNS_NAME_PATTERN = re.compile( + r"(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+" + r"[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\Z" +) class ConfigurationError(ValueError): @@ -259,19 +266,40 @@ def from_environ(cls, environ: Mapping[str, str]) -> Config: issuer_port = parsed_issuer.port except ValueError as error: raise ConfigurationError("WORKLOAD_ISSUER is not a valid URL") from error - if ( - parsed_issuer.scheme != "http" - or parsed_issuer.hostname != bind_host - or issuer_port != port - or parsed_issuer.netloc != f"{bind_host}:{port}" - or parsed_issuer.path - or parsed_issuer.query - or parsed_issuer.fragment + has_unsafe_url_component = ( + bool(parsed_issuer.path) + or bool(parsed_issuer.query) + or bool(parsed_issuer.fragment) or parsed_issuer.username is not None or parsed_issuer.password is not None - ): + ) + local_issuer = ( + parsed_issuer.scheme == "http" + and parsed_issuer.hostname == bind_host + and issuer_port == port + and parsed_issuer.netloc == f"{bind_host}:{port}" + and not has_unsafe_url_component + ) + hosted_hostname = parsed_issuer.hostname or "" + try: + ipaddress.ip_address(hosted_hostname) + except ValueError: + hosted_hostname_is_ip = False + else: + hosted_hostname_is_ip = True + hosted_issuer = ( + parsed_issuer.scheme == "https" + and not hosted_hostname_is_ip + and DNS_NAME_PATTERN.fullmatch(hosted_hostname) is not None + and issuer_port in (None, 443) + and parsed_issuer.netloc + in (hosted_hostname, f"{hosted_hostname}:443") + and not has_unsafe_url_component + ) + if not local_issuer and not hosted_issuer: raise ConfigurationError( - "WORKLOAD_ISSUER must exactly match the configured loopback listener" + "WORKLOAD_ISSUER must be the exact loopback listener or a " + "path-free HTTPS DNS origin" ) token_ttl_seconds = _bounded_integer( diff --git a/scripts/test_hosted_relay_bundles.py b/scripts/test_hosted_relay_bundles.py new file mode 100644 index 0000000..61fdc99 --- /dev/null +++ b/scripts/test_hosted_relay_bundles.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import importlib.util +import sys +import tempfile +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_checker(): + spec = importlib.util.spec_from_file_location( + "check_hosted_relay_bundles", + ROOT / "scripts" / "check-hosted-relay-bundles.py", + ) + if spec is None or spec.loader is None: + raise RuntimeError("could not load check-hosted-relay-bundles.py") + module = importlib.util.module_from_spec(spec) + sys.modules["check_hosted_relay_bundles"] = module + spec.loader.exec_module(module) + return module + + +class HostedRelayBundleTests(unittest.TestCase): + def test_artifact_closure_accepts_identical_complete_trees(self) -> None: + checker = load_checker() + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + bundled = root / "bundled" + source = root / "source" + for directory in (bundled, source): + (directory / "contracts").mkdir(parents=True) + (directory / "contracts" / "contract.json").write_text( + '{"version":1}\n', encoding="utf-8" + ) + + checker.verify_artifact_closure("example", bundled, source) + + def test_artifact_closure_rejects_changed_or_missing_files(self) -> None: + checker = load_checker() + with tempfile.TemporaryDirectory() as temporary_directory: + root = Path(temporary_directory) + bundled = root / "bundled" + source = root / "source" + bundled.mkdir() + source.mkdir() + (bundled / "contract.json").write_text("signed", encoding="utf-8") + (source / "contract.json").write_text("changed", encoding="utf-8") + + with self.assertRaisesRegex( + SystemExit, "signed artifact contract.json differs" + ): + checker.verify_artifact_closure("example", bundled, source) + + (source / "extra.json").write_text("extra", encoding="utf-8") + with self.assertRaisesRegex(SystemExit, "artifact paths differ"): + checker.verify_artifact_closure("example", bundled, source) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/test_quality_scripts.py b/scripts/test_quality_scripts.py index 8499b47..5eff017 100644 --- a/scripts/test_quality_scripts.py +++ b/scripts/test_quality_scripts.py @@ -360,6 +360,7 @@ def test_workload_issuer_contract_is_bounded_and_esignet_isolated(self) -> None: (ROOT / "compose.coolify.esignet.yaml").read_text(encoding="utf-8") ) hosted_esignet = hosted_compose["services"]["esignet"] + hosted_agent = hosted_compose["services"]["nia-esignet-workload-agent"] for esignet in (local_esignet, hosted_esignet): self.assertEqual( esignet["environment"]["REGISTRY_RELAY_AUTH_BEARER_TOKEN_FILE"], @@ -376,6 +377,36 @@ def test_workload_issuer_contract_is_bounded_and_esignet_isolated(self) -> None: local_esignet["depends_on"]["nia-workload-agent"]["condition"], "service_healthy", ) + self.assertEqual( + hosted_esignet["depends_on"]["nia-esignet-workload-agent"]["condition"], + "service_healthy", + ) + hosted_identity = json.loads( + hosted_agent["environment"]["WORKLOAD_IDENTITIES_JSON"] + ) + self.assertEqual( + hosted_agent["environment"]["WORKLOAD_ISSUER"], + "https://workload-issuer.solmara.registrystack.org", + ) + self.assertEqual( + hosted_agent["environment"]["NIA_ESIGNET_RELAY_WORKLOAD_JWK"], + "${NIA_ESIGNET_RELAY_WORKLOAD_JWK}", + ) + self.assertEqual( + hosted_identity, + [ + { + "audience": "registry-relay", + "azp": "solmara-esignet", + "subject": "solmara-esignet", + "scopes": ["population:identity_release"], + "token_file": "/run/secrets/solmara-esignet-relay-token", + "private_jwk_env": "NIA_ESIGNET_RELAY_WORKLOAD_JWK", + "token_uid": 1001, + "token_gid": 1001, + } + ], + ) self.assertNotIn("ports", local_esignet) local_esignet_edge = yaml.safe_load( (ROOT / "compose.esignet.yaml").read_text(encoding="utf-8") @@ -391,13 +422,7 @@ def test_workload_issuer_contract_is_bounded_and_esignet_isolated(self) -> None: "config/esignet/nginx.conf", local_esignet_edge["build"]["args"]["ESIGNET_NGINX_CONF"], ) - self.assertEqual( - hosted_compose["volumes"]["nia-esignet-workload-token"], - { - "external": True, - "name": "${NIA_ESIGNET_WORKLOAD_TOKEN_VOLUME:-solmara-nia-esignet-workload-token}", - }, - ) + self.assertIsNone(hosted_compose["volumes"]["nia-esignet-workload-token"]) hosted_interior = yaml.safe_load( (ROOT / "compose.coolify.interior.yaml").read_text(encoding="utf-8") @@ -410,6 +435,39 @@ def test_workload_issuer_contract_is_bounded_and_esignet_isolated(self) -> None: ) ) + jwks = json.loads( + ( + ROOT / "metadata" / "public" / ".well-known" / "jwks.json" + ).read_text(encoding="utf-8") + ) + expected_kids = { + value + for value in load_secret_generator().JWK_KIDS.values() + if "relay-workload-key" in value + } + self.assertEqual({key["kid"] for key in jwks["keys"]}, expected_kids) + self.assertEqual(len(jwks["keys"]), len(expected_kids)) + for key in jwks["keys"]: + self.assertEqual(set(key), {"alg", "crv", "kid", "kty", "x"}) + self.assertEqual(key["alg"], "EdDSA") + self.assertEqual(key["crv"], "Ed25519") + self.assertEqual(key["kty"], "OKP") + + hosted_core = yaml.safe_load( + (ROOT / "compose.coolify.yaml").read_text(encoding="utf-8") + ) + issuer = hosted_core["services"]["workload-issuer"] + self.assertEqual( + issuer["labels"]["solmara.lab.host"], + "workload-issuer.solmara.registrystack.org", + ) + self.assertTrue( + any( + "/.well-known/jwks.json" in part + for part in issuer["healthcheck"]["test"] + ) + ) + retired_static_names = { "SOLMARA_ESIGNET_IDENTITY_RELEASE_RAW", "SOLMARA_ESIGNET_IDENTITY_RELEASE_HASH", @@ -935,6 +993,8 @@ def test_coolify_authority_state_is_postgresql_isolated(self) -> None: relay = services[relay_name] notary = services[notary_name] installer = services[f"{notary_name}-state-install"] + workload_agent_name = f"{key}-workload-agent" + workload_agent = services[workload_agent_name] bootstrap = services[f"{key}-relay-state-bootstrap"] config_state_init = services[ f"{key}-relay-config-state-init" @@ -1010,6 +1070,43 @@ def test_coolify_authority_state_is_postgresql_isolated(self) -> None: ], "service_completed_successfully", ) + self.assertEqual( + notary["depends_on"][workload_agent_name]["condition"], + "service_healthy", + ) + self.assertEqual( + installer["depends_on"][workload_agent_name]["condition"], + "service_healthy", + ) + workload_environment = workload_agent["environment"] + workload_identity = json.loads( + workload_environment["WORKLOAD_IDENTITIES_JSON"] + ) + self.assertEqual( + workload_environment["WORKLOAD_ISSUER"], + "https://workload-issuer.solmara.registrystack.org", + ) + private_jwk_env = f"{key.upper()}_RELAY_WORKLOAD_JWK" + self.assertEqual( + workload_environment[private_jwk_env], + f"${{{private_jwk_env}}}", + ) + self.assertEqual(len(workload_identity), 1) + self.assertEqual( + workload_identity[0]["private_jwk_env"], + private_jwk_env, + ) + self.assertEqual( + workload_identity[0]["token_file"], + f"/run/secrets/{key}-notary-relay-token", + ) + self.assertIn( + f"{key}-workload-token:/run/secrets", + workload_agent["volumes"], + ) + self.assertIsNone( + compose["volumes"][f"{key}-workload-token"] + ) self.assertEqual( notary["labels"]["solmara.lab.host"], f"{notary_name}.solmara.registrystack.org", diff --git a/scripts/test_relay_workload_identity_agent.py b/scripts/test_relay_workload_identity_agent.py index 5ad1600..96167bc 100644 --- a/scripts/test_relay_workload_identity_agent.py +++ b/scripts/test_relay_workload_identity_agent.py @@ -138,6 +138,22 @@ def __call__(self) -> float: class WorkloadIdentityAgentTests(unittest.TestCase): + def test_https_dns_issuer_is_independent_from_loopback_listener(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + environment, _ = valid_environment(Path(temporary_directory)) + environment["WORKLOAD_ISSUER"] = ( + "https://workload-issuer.solmara.registrystack.org" + ) + + config = agent.Config.from_environ(environment) + + self.assertEqual(config.bind_host, "127.0.0.1") + self.assertEqual(config.port, 8090) + self.assertEqual( + config.issuer, + "https://workload-issuer.solmara.registrystack.org", + ) + def test_each_identity_gets_exact_claims_and_its_own_signature(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: directory = Path(temporary_directory) @@ -399,9 +415,19 @@ def identities(change: dict[str, object]) -> str: cases: dict[str, dict[str, str | None]] = { "non_loopback_bind": {"WORKLOAD_BIND_HOST": "0.0.0.0"}, "non_loopback_issuer": {"WORKLOAD_ISSUER": "http://localhost:8090"}, - "https_issuer": {"WORKLOAD_ISSUER": "https://127.0.0.1:8090"}, + "https_ip_issuer": {"WORKLOAD_ISSUER": "https://127.0.0.1"}, + "https_localhost_issuer": {"WORKLOAD_ISSUER": "https://localhost"}, + "https_userinfo_issuer": { + "WORKLOAD_ISSUER": "https://user@example.test" + }, + "https_nonstandard_port": { + "WORKLOAD_ISSUER": "https://issuer.example.test:8443" + }, "malformed_issuer": {"WORKLOAD_ISSUER": "http://["}, "issuer_path": {"WORKLOAD_ISSUER": "http://127.0.0.1:8090/issuer"}, + "https_issuer_path": { + "WORKLOAD_ISSUER": "https://issuer.example.test/issuer" + }, "issuer_port_mismatch": {"WORKLOAD_PORT": "8091"}, "blank_audience": { "WORKLOAD_IDENTITIES_JSON": identities({"audience": ""}) From 1eda114adf15b64461dbbb3c4d3f99229437455e Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 11:12:58 +0700 Subject: [PATCH 12/14] Pin reloadable eSignet Relay credential Signed-off-by: Jeremi Joslin --- .github/workflows/release-candidate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 1ae5ceb..7986277 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -33,7 +33,7 @@ jobs: ESIGNET_POSTGRES_IMAGE: postgres:16 ESIGNET_BASE_IMAGE: mosipid/esignet-with-plugins:1.8.0 ESIGNET_UI_IMAGE: mosipid/oidc-ui:1.8.0 - ESIGNET_RELAY_AUTHENTICATOR_REF: 9c066ae897cd3966333d7d75fc77db2e6dc7342c + ESIGNET_RELAY_AUTHENTICATOR_REF: 2e7d0e452a778e048f2c259391397d1e1844318e STATIC_METADATA_IMAGE: python:3.12-alpine steps: - uses: actions/checkout@v4 From fef9eb35692b89ac67fdd45ecccfeaef4b3a58c0 Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 12:06:03 +0700 Subject: [PATCH 13/14] Fix hosted deployment verification Signed-off-by: Jeremi Joslin --- README.md | 4 ++-- home/e2e/home.test.ts | 11 +++++++++-- scripts/smoke-hosted.py | 29 ++++++++++++----------------- scripts/test_smoke_hosted.py | 16 ++++++++++++++++ 4 files changed, 39 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index bed8028..6a73e01 100644 --- a/README.md +++ b/README.md @@ -201,8 +201,8 @@ candidate-only gate locally with `just review-release `; the normal contributor and CI gate remains `just review`. For local eSignet testing, run `just up-esignet` instead of `just up`, then -sign in through the portal with Elena's fixture `legacy_nid` and static OTP -`111111`. This sign-in is the end-to-end check of the NIA +sign in through the portal with Elena's fixture UIN `2300018263` and static +OTP `111111`. This sign-in is the end-to-end check of the NIA `solmara-nia-userinfo` attribute-release profile and its rotating eSignet workload identity. Run `just smoke-esignet` for the public discovery checks. diff --git a/home/e2e/home.test.ts b/home/e2e/home.test.ts index de7a6e9..73bd29d 100644 --- a/home/e2e/home.test.ts +++ b/home/e2e/home.test.ts @@ -2,6 +2,9 @@ import { expect, test } from '@playwright/test'; const NAV_LINKS = ['Stories', 'Explorer', 'Purposes', 'Problem codes', 'Anatomy', 'Status']; +const evaluationUrl = (configuredUrl: string | undefined, fallbackUrl: string) => + `${(configuredUrl ?? fallbackUrl).replace(/\/+$/, '')}/v1/evaluations`; + test('landing renders with header nav and every section in order', async ({ page }) => { const response = await page.goto('/'); expect(response?.headers()['content-security-policy']).toContain("default-src 'self'"); @@ -195,8 +198,12 @@ test('citizen story renders runnable curls for each authority call', async ({ pa const authorityRequests = result.locator('.request-list .peer-call'); await expect(authorityRequests).toHaveCount(2); await expect(authorityRequests.getByRole('button', { name: 'Copy as curl' })).toHaveCount(2); - await expect(authorityRequests.nth(0)).toContainText('http://localhost:4325/v1/evaluations'); - await expect(authorityRequests.nth(1)).toContainText('http://localhost:4326/v1/evaluations'); + await expect(authorityRequests.nth(0)).toContainText( + evaluationUrl(process.env.CRA_NOTARY_URL, 'http://localhost:4325') + ); + await expect(authorityRequests.nth(1)).toContainText( + evaluationUrl(process.env.NIA_NOTARY_URL, 'http://localhost:4326') + ); await expect(result).not.toContainText('solmara://authority-notaries'); }); diff --git a/scripts/smoke-hosted.py b/scripts/smoke-hosted.py index 763484a..13a9f67 100755 --- a/scripts/smoke-hosted.py +++ b/scripts/smoke-hosted.py @@ -90,16 +90,7 @@ def main(argv: list[str] | None = None) -> int: ( "eSignet backend", lambda: run_command( - [ - sys.executable, - str(ROOT / "scripts" / "smoke-esignet.py"), - "--relay-url", - relay_url(targets, "SOLMARA_NIA_RELAY_URL"), - "--esignet-url", - targets.esignet_url, - "--esignet-ui-url", - targets.esignet_ui_url, - ], + esignet_smoke_command(targets), env, cwd=ROOT, ), @@ -184,6 +175,17 @@ def main(argv: list[str] | None = None) -> int: return 0 +def esignet_smoke_command(targets: HostedTargets) -> list[str]: + return [ + sys.executable, + str(ROOT / "scripts" / "smoke-esignet.py"), + "--esignet-url", + targets.esignet_url, + "--esignet-ui-url", + targets.esignet_ui_url, + ] + + def parse_args(argv: list[str] | None) -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( @@ -343,13 +345,6 @@ def hosted_env( return env -def relay_url(targets: HostedTargets, env_name: str) -> str: - for relay in targets.relays: - if relay.env_name == env_name: - return relay.base_url - raise SmokeFailure(f"missing relay target for {env_name}") - - def check_public_routes(targets: HostedTargets, timeout: float) -> None: checks = [ ServiceTarget("Visitor Center", targets.home_url, "/"), diff --git a/scripts/test_smoke_hosted.py b/scripts/test_smoke_hosted.py index f0008c3..d9866a7 100644 --- a/scripts/test_smoke_hosted.py +++ b/scripts/test_smoke_hosted.py @@ -238,6 +238,22 @@ def test_normalize_argv_accepts_just_separator(self) -> None: ) self.assertEqual(smoke_hosted.normalize_argv(["--browser"]), ["--browser"]) + def test_esignet_smoke_uses_only_supported_discovery_arguments(self) -> None: + targets = smoke_hosted.default_targets("solmara.registrystack.org") + + command = smoke_hosted.esignet_smoke_command(targets) + + self.assertNotIn("--relay-url", command) + self.assertEqual( + command[-4:], + [ + "--esignet-url", + "https://esignet.solmara.registrystack.org", + "--esignet-ui-url", + "https://esignet-ui.solmara.registrystack.org", + ], + ) + def test_home_demo_accepts_expected_scenario_flow(self) -> None: with StubServer(hosted_routes()) as server: smoke_hosted.check_home_demo(server.url, timeout=2) From e2692df6d3354f90d392efa2e05af2377352c5ce Mon Sep 17 00:00:00 2001 From: Jeremi Joslin Date: Sun, 26 Jul 2026 23:16:03 +0700 Subject: [PATCH 14/14] fix(solmara): accept portable registryctl build output Signed-off-by: Jeremi Joslin --- scripts/registryctl-build-output.py | 8 +++++--- scripts/test_registryctl_build_output.py | 23 +++++++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/scripts/registryctl-build-output.py b/scripts/registryctl-build-output.py index 1393ced..f67d889 100755 --- a/scripts/registryctl-build-output.py +++ b/scripts/registryctl-build-output.py @@ -56,12 +56,14 @@ def parse_build_output( if not isinstance(output_value, str) or not output_value: raise BuildReportError("the JSON report has no output root") output = Path(output_value) - if not output.is_absolute(): - raise BuildReportError("the JSON report output root is not absolute") try: project_root = project_directory.resolve(strict=True) - output_root = output.resolve(strict=True) + output_root = ( + output.resolve(strict=True) + if output.is_absolute() + else (project_root / output).resolve(strict=True) + ) output_root.relative_to(project_root) except (FileNotFoundError, RuntimeError, ValueError) as error: raise BuildReportError( diff --git a/scripts/test_registryctl_build_output.py b/scripts/test_registryctl_build_output.py index 54a1834..fb14e67 100644 --- a/scripts/test_registryctl_build_output.py +++ b/scripts/test_registryctl_build_output.py @@ -59,6 +59,16 @@ def test_accepts_the_versioned_project_owned_build_root(self) -> None: self.output.resolve(), ) + def test_accepts_the_portable_project_relative_build_root(self) -> None: + self.assertEqual( + MODULE.parse_build_output( + self.report(output=".registry-stack/build/local"), + project_directory=self.project, + environment="local", + ), + self.output.resolve(), + ) + def test_rejects_the_wrong_environment_without_echoing_report_values(self) -> None: with self.assertRaisesRegex( MODULE.BuildReportError, @@ -84,6 +94,19 @@ def test_rejects_an_output_root_outside_the_project(self) -> None: environment="local", ) + def test_rejects_a_relative_output_root_that_escapes_the_project(self) -> None: + outside = Path(self.temporary.name) / "outside" + outside.mkdir() + with self.assertRaisesRegex( + MODULE.BuildReportError, + "not a real project-owned directory", + ): + MODULE.parse_build_output( + self.report(output="../outside"), + project_directory=self.project, + environment="local", + ) + def test_rejects_an_incomplete_product_closure(self) -> None: (self.output / "private" / "notary" / "config" / "notary.yaml").unlink() with self.assertRaisesRegex(