From fc9e1a497c2158b578fbacb8ce0ed60105a827a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20Bajto=C5=A1?= Date: Fri, 26 Jun 2026 09:26:06 +0200 Subject: [PATCH 01/67] feat: add staging deployment bundles and tooling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compose the Forge services into two version-pinned staging bundles for the Calibnet box: `core` (sprue + signing-service + delegator + postgres/minio/dynamodb) and `piri` (one storage node), wired over public https://*.staging.fil.one URLs fronted by the host Caddy. No indexer/IPNI/redis/Anvil/mailer; the host Lotus node is reached via host-gateway. Add `smelt staging keygen` to generate service identities, EVM wallets, and UCAN proofs once — storing private keys in 1Password, committing the proofs, and recording the public wallet addresses in wallets.env for periodic top-ups. Contract addresses live once in smart-contracts.env; the provision script renders configs (op inject + ${VAR}) and streams them to the box with no local plaintext; the deploy script pulls pinned images and verifies health. Includes a box bootstrap script and the docs/STAGING_DEPLOY.md runbook. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Miroslav Bajtoš --- Makefile | 40 ++- cmd/smelt/cmd/staging.go | 89 +++++++ docs/STAGING_DEPLOY.md | 220 +++++++++++++++++ environments/staging/README.md | 14 ++ .../staging/caddy/forge-staging.caddy | 31 +++ environments/staging/core/compose.yml | 195 +++++++++++++++ environments/staging/core/config.env | 24 ++ .../config/delegator/delegator.yaml.example | 32 +++ .../core/config/delegator/delegator.yaml.tpl | 43 ++++ .../staging/core/config/signer/signer.yaml | 11 + .../core/config/sprue/config.yaml.example | 40 +++ .../staging/core/config/sprue/config.yaml.tpl | 48 ++++ environments/staging/core/secrets.env.example | 10 + environments/staging/core/secrets.env.tpl | 20 ++ environments/staging/core/versions.env | 20 ++ environments/staging/dns/fil-one-staging.tf | 32 +++ environments/staging/piri/compose.yml | 53 ++++ environments/staging/piri/config.env | 21 ++ .../config/piri/piri-base-config.toml.example | 30 +++ .../config/piri/piri-base-config.toml.tpl | 38 +++ environments/staging/piri/entrypoint.sh | 81 ++++++ environments/staging/piri/versions.env | 6 + environments/staging/proofs/.gitkeep | 2 + environments/staging/smart-contracts.env | 33 +++ environments/staging/wallets.env | 12 + pkg/generate/keys.go | 7 + pkg/staging/keygen.go | 231 ++++++++++++++++++ pkg/staging/onepassword.go | 67 +++++ pkg/staging/proofs.go | 97 ++++++++ pkg/staging/wallet.go | 91 +++++++ pkg/staging/wallet_test.go | 80 ++++++ scripts/staging-bootstrap.sh | 81 ++++++ scripts/staging-deploy.sh | 72 ++++++ scripts/staging-provision.sh | 98 ++++++++ 34 files changed, 1968 insertions(+), 1 deletion(-) create mode 100644 cmd/smelt/cmd/staging.go create mode 100644 docs/STAGING_DEPLOY.md create mode 100644 environments/staging/README.md create mode 100644 environments/staging/caddy/forge-staging.caddy create mode 100644 environments/staging/core/compose.yml create mode 100644 environments/staging/core/config.env create mode 100644 environments/staging/core/config/delegator/delegator.yaml.example create mode 100644 environments/staging/core/config/delegator/delegator.yaml.tpl create mode 100644 environments/staging/core/config/signer/signer.yaml create mode 100644 environments/staging/core/config/sprue/config.yaml.example create mode 100644 environments/staging/core/config/sprue/config.yaml.tpl create mode 100644 environments/staging/core/secrets.env.example create mode 100644 environments/staging/core/secrets.env.tpl create mode 100644 environments/staging/core/versions.env create mode 100644 environments/staging/dns/fil-one-staging.tf create mode 100644 environments/staging/piri/compose.yml create mode 100644 environments/staging/piri/config.env create mode 100644 environments/staging/piri/config/piri/piri-base-config.toml.example create mode 100644 environments/staging/piri/config/piri/piri-base-config.toml.tpl create mode 100755 environments/staging/piri/entrypoint.sh create mode 100644 environments/staging/piri/versions.env create mode 100644 environments/staging/proofs/.gitkeep create mode 100644 environments/staging/smart-contracts.env create mode 100644 environments/staging/wallets.env create mode 100644 pkg/staging/keygen.go create mode 100644 pkg/staging/onepassword.go create mode 100644 pkg/staging/proofs.go create mode 100644 pkg/staging/wallet.go create mode 100644 pkg/staging/wallet_test.go create mode 100755 scripts/staging-bootstrap.sh create mode 100755 scripts/staging-deploy.sh create mode 100755 scripts/staging-provision.sh diff --git a/Makefile b/Makefile index 4ce3c79..f85e515 100644 --- a/Makefile +++ b/Makefile @@ -38,7 +38,7 @@ workspace-build: rm -f $(WORKSPACE_OVERRIDE); \ fi -.PHONY: help generate init up down restart clean nuke fresh logs pull build cli status guppy regen debug-upload ensure-state check-docker workspace-build +.PHONY: help generate init up down restart clean nuke fresh logs pull build cli status guppy regen debug-upload ensure-state check-docker workspace-build staging-keygen staging-bootstrap staging-provision-core staging-provision-piri staging-deploy-core staging-deploy-piri # Default target - show help help: @@ -82,6 +82,14 @@ help: @echo "Debugging:" @echo " make debug-upload Run upload (sprue) under Delve on localhost:2345" @echo "" + @echo "Staging deployment (see docs/STAGING_DEPLOY.md):" + @echo " make staging-keygen One-time: generate keys+wallets+proofs, store in 1Password" + @echo " make staging-bootstrap One-time: prepare the box (repo, dirs, Caddy) — needs REPO_URL" + @echo " make staging-provision-core Render core secrets from 1Password and ship to the box (dev machine only)" + @echo " make staging-provision-piri Render piri secrets from 1Password and ship to the box (dev machine only)" + @echo " make staging-deploy-core Deploy the core bundle (sprue + signing-service + delegator)" + @echo " make staging-deploy-piri Deploy the piri bundle" + @echo "" @echo "Options:" @echo " YES=1 Skip confirmation prompts (e.g., make nuke YES=1)" @echo " SMELT_WORKSPACE=1 Run containers against binaries built from your local" @@ -255,6 +263,36 @@ regen: @echo "Keys and proofs regenerated." @echo "Run 'make clean && make up' to restart services with new keys." +# --- Staging deployment --------------------------------------------------- +# Thin wrappers over the staging tooling; full runbook in docs/STAGING_DEPLOY.md. + +# One-time: generate staging keys, EVM wallets, and UCAN proofs; store private +# keys in 1Password; write proofs to environments/staging/proofs/ (commit them); +# write PAYER_ADDRESS into environments/staging/smart-contracts.env (commit it). +staging-keygen: + @go run ./cmd/smelt staging keygen + +# One-time: prepare the box — clone/update the repo, create secrets + data dirs, +# wire the Caddy snippet, verify. Pass REPO_URL on first run. +staging-bootstrap: + @./scripts/staging-bootstrap.sh + +# Render configs/keys from 1Password and stream them to the box. Developer +# machine only (needs your op session + SSH); never run from CI. Per-bundle, since +# we typically deploy one bundle at a time. +staging-provision-core: + @./scripts/staging-provision.sh core + +staging-provision-piri: + @./scripts/staging-provision.sh piri + +# Deploy a bundle: pull pinned images, recreate, verify health. +staging-deploy-core: + @./scripts/staging-deploy.sh core + +staging-deploy-piri: + @./scripts/staging-deploy.sh piri + # Pull latest pre-built images (ignores failures for local-only images) pull: generated/compose/piri.yml ensure-state $(COMPOSE) pull --ignore-pull-failures diff --git a/cmd/smelt/cmd/staging.go b/cmd/smelt/cmd/staging.go new file mode 100644 index 0000000..4c8210a --- /dev/null +++ b/cmd/smelt/cmd/staging.go @@ -0,0 +1,89 @@ +package cmd + +import ( + "fmt" + + "github.com/fil-forge/smelt/pkg/staging" + "github.com/spf13/cobra" +) + +var stagingCmd = &cobra.Command{ + Use: "staging", + Short: "Manage the Forge staging deployment", +} + +var stagingKeygenCmd = &cobra.Command{ + Use: "keygen", + Short: "One-time generation of staging keys, wallets, and proofs", + Long: `Generates the staging stack's long-lived secrets ONCE and stores them in +1Password, and writes the (non-secret) UCAN delegation proofs into +environments/staging/proofs/ to be committed. + +It generates: + - Ed25519 service identity keys (PEM) + - real random secp256k1 EVM wallets for the payer, delegator transactor, and + piri owner — their addresses are printed so you can fund them via a Calibnet + faucet (private keys are stored in 1Password, never printed) + - random Postgres/MinIO connection secrets + - the indexing/egress/piri UCAN delegation proofs + +This is a deliberate one-shot: re-running mints fresh keys and overwrites the +1Password item, orphaning any already-funded wallets. Run it again only when +intentionally rotating the whole staging identity set.`, + RunE: runStagingKeygen, +} + +func init() { + rootCmd.AddCommand(stagingCmd) + stagingCmd.AddCommand(stagingKeygenCmd) + stagingKeygenCmd.Flags().StringP("project-dir", "d", ".", "project root directory") + stagingKeygenCmd.Flags().String("op-vault", "Fil One", "1Password vault") + stagingKeygenCmd.Flags().String("op-item", "FilOne Forge Staging", "1Password item title") + stagingKeygenCmd.Flags().Bool("store", true, "store generated secrets into 1Password via the op CLI") + stagingKeygenCmd.Flags().Bool("proofs", true, "generate UCAN delegation proofs (requires ucantool)") + stagingKeygenCmd.Flags().String("ucantool", "ucantool", "ucantool binary name or path") +} + +func runStagingKeygen(cmd *cobra.Command, args []string) error { + projectDir, _ := cmd.Flags().GetString("project-dir") + opVault, _ := cmd.Flags().GetString("op-vault") + opItem, _ := cmd.Flags().GetString("op-item") + store, _ := cmd.Flags().GetBool("store") + proofs, _ := cmd.Flags().GetBool("proofs") + ucantool, _ := cmd.Flags().GetString("ucantool") + + result, err := staging.Keygen(staging.Options{ + ProjectDir: projectDir, + OPVault: opVault, + OPItem: opItem, + Store: store, + Proofs: proofs, + Ucantool: ucantool, + }) + if err != nil { + return err + } + + fmt.Println("Staging keygen complete.") + if len(result.ProofsWritten) > 0 { + fmt.Printf("\nProofs written (commit these):\n") + for _, p := range result.ProofsWritten { + fmt.Printf(" %s\n", p) + } + } + if len(result.OPFields) > 0 { + fmt.Printf("\nStored %d secret field(s) in 1Password item %q (vault %q):\n", + len(result.OPFields), opItem, opVault) + for _, f := range result.OPFields { + fmt.Printf(" %s\n", f) + } + } + if result.WalletsEnvPath != "" { + fmt.Printf("\nWrote wallet addresses to %s (commit it).\n", result.WalletsEnvPath) + } + fmt.Printf("\nFund these wallets on the Calibnet faucet (https://faucet.calibnet.chainsafe-fil.io):\n") + for role, addr := range result.FundAddresses { + fmt.Printf(" %-24s %s\n", role+":", addr) + } + return nil +} diff --git a/docs/STAGING_DEPLOY.md b/docs/STAGING_DEPLOY.md new file mode 100644 index 0000000..8840f93 --- /dev/null +++ b/docs/STAGING_DEPLOY.md @@ -0,0 +1,220 @@ +# Staging Deployment Runbook + +How to manually deploy the Forge stack to the **staging** box. + +> **Scope (first step):** manual deploy, no CI. Two independently-deployed Compose +> bundles on one VM, secrets injected from 1Password at provision time. + +## Architecture & rationale + +Forge services are each released independently to GHCR. Staging composes them into +**version-pinned** Compose manifests held in this repo — the unit of deployment. The +design rests on four ideas: + +1. **One version-pinned artifact per bundle.** Each bundle's `versions.env` pins exact + image references (digests in production use). A deploy applies one set; rollback is + re-deploying a previous pinned commit. No rolling `:latest`/`:main`. +2. **Two bundles, deployed independently.** `core` (sprue + signing-service + delegator + + their dependency containers) and `piri` (the storage node) deploy and roll back on + their own cadence. They communicate over **public `https://*.staging.fil.one` URLs** + (fronted by the host's Caddy), not single-network Docker DNS — so the split is real. +3. **Dependencies are per-environment.** Postgres + S3 (MinIO) + DynamoDB run as + containers in the stack; persistent data lives on the box's ZFS pool + (`/mnt/data/fil-one/forge`). The chain RPC is the **host Lotus node** (Calibnet, + `0.0.0.0:1234`), reached via `host.docker.internal:host-gateway` — no Anvil. +4. **Config in Git, secrets in 1Password.** Non-secret config (compose, `config.env`, + committed proofs) lives in the repo. Secret values live in the single 1Password item + `op://Fil One/FilOne Forge Staging` and are rendered onto the box at provision time — + never committed, never written to a developer's local disk, never in CI. + +There is **no** indexer / IPNI / redis / Anvil / mailer in staging. (sprue runs with an +empty `indexer.endpoint` and `mailer: nop`; the delegator still validates indexing/egress +delegations at startup, so those proofs are generated even though neither service runs.) + +## Topology + +- **Box:** `root@23.83.66.244` (Servers.com Calibnet, hostname `ff`), Ubuntu, key-only SSH. +- **Bundle `core`** — `sprue` (upload) + `signing-service` + `delegator` + `postgres` + + `minio` + `dynamodb-local`. Project `forge-staging-core`. +- **Bundle `piri`** — one `piri-0` storage node (sqlite + filesystem). Project `forge-staging-piri`. +- The bundles talk over **public `https://*.staging.fil.one` URLs**, fronted by the host's + existing Caddy (`caddy-guppy.service`). The host Lotus Eth RPC (`0.0.0.0:1234`) is reached + from containers via `host.docker.internal:host-gateway`. +- **No** indexer / IPNI / redis / Anvil / smtp4dev in staging. + +Host layout: + +``` +/root/fil-one/forge/ # this repo, checked out on the box + environments/staging/smart-contracts.env # single source: chain id, RPC, contract addresses + environments/staging/wallets.env # wallet addresses (public), written by keygen + environments/staging/{core,piri}/... # bundle manifests + committed config + environments/staging/proofs/*.txt # committed UCAN proofs + caddy/forge-staging.caddy # copied from environments/staging/caddy/ +/root/fil-one/forge/secrets/ # provisioned (rendered) files (NOT in git) + sprue-config.yaml delegator.yaml secrets.env piri-base-config.toml + sprue.pem signing-service.pem delegator.pem payer-key.hex + piri-0.pem piri-0-wallet.hex +/mnt/data/fil-one/forge/ # persistent data on the ZFS pool + postgres/ minio/ dynamodb/ piri-0/ +``` + +## Prerequisites + +- **DNS:** A records for `sprue` / `signing-service` / `delegator` / `piri-0` under + `staging.fil.one` → `23.83.66.244`, `proxied = false`. Apply + [`environments/staging/dns/fil-one-staging.tf`](../environments/staging/dns/fil-one-staging.tf) + via `fil-one/infrastructure`. +- **Calibnet Forge contract addresses** — already filled in + [`environments/staging/smart-contracts.env`](../environments/staging/smart-contracts.env), + the **single source of truth** for the chain id, RPC URL, and every contract address. + Configs reference these as `${VAR}` and provision renders them in — no duplication, + nothing to fill by hand. Wallet addresses live in + [`environments/staging/wallets.env`](../environments/staging/wallets.env), written by keygen. +- **Dev machine:** `op` (1Password CLI, signed in), `ssh` to the box, `go` (for keygen), + `ucantool` (`go install github.com/fil-forge/ucantool@latest`). + +## 1. One-time: generate keys, wallets, proofs + +Run once, ever (re-running mints fresh keys and overwrites the 1Password item): + +```bash +make staging-keygen # = go run ./cmd/smelt staging keygen +``` + +This generates the Ed25519 identities, three random EVM wallets (payer / delegator +transactor / piri owner), Postgres + MinIO secrets, stores all private material in the +single 1Password item `op://Fil One/FilOne Forge Staging`, writes the UCAN proofs to +`environments/staging/proofs/`, and **writes the three public wallet addresses into +`environments/staging/wallets.env`** (`PAYER_ADDRESS` from there renders into piri's +config). It prints the three EVM addresses. + +## 2. Fund the wallets and commit + +Fund all three addresses (in `wallets.env`) from the +[Calibnet faucet](https://faucet.calibnet.chainsafe-fil.io), then commit the generated +artifacts: + +```bash +git add environments/staging/proofs environments/staging/wallets.env +git commit -m "staging: add delegation proofs and wallet addresses" +``` + +Contract addresses are already set in `smart-contracts.env`, so there is nothing else to +fill in. Keep `wallets.env` handy — top up these balances periodically. (Mailer is `nop` — +there is no email login in staging; see Known risks.) + +## 3. Bootstrap the box (first time only) + +Clones/updates the repo on the box, creates the secrets + data directories, wires the +Forge Caddy snippet into the host's main Caddyfile, and verifies did:web endpoints +(warn-only until the services are deployed). Idempotent. + +```bash +REPO_URL=git@github.com:fil-forge/smelt.git make staging-bootstrap +``` + +(Override `FORGE_HOST`, `FORGE_DIR`, `MAIN_CADDYFILE`, `CADDY_SERVICE`, … as env vars; see +`scripts/staging-bootstrap.sh`.) + +## 4. Provision secrets onto the box (dev machine) + +Provision the bundle you're about to deploy (we typically deploy one at a time): + +```bash +op signin +make staging-provision-core # sprue-config.yaml, delegator.yaml, secrets.env + key files +make staging-provision-piri # piri-0 key files +``` + +Renders configs — `op inject` resolves `{{ op:// }}` secret refs and `${VAR}` refs are +substituted from `smart-contracts.env` + `wallets.env` (core renders sprue/delegator configs ++ `secrets.env`; piri renders its base config) — and ships key files (via `op read`) into +`/root/fil-one/forge/secrets/`, atomically, no plaintext on your local disk. (piri provision +fails fast if `PAYER_ADDRESS` isn't set in `wallets.env` yet.) Re-run only when a config +template, secret, or env value changes. + +## 5. Deploy + +Pull the on-box checkout to the commit you're deploying first, then: + +```bash +make staging-deploy-core # sprue + signing-service + delegator + deps +make staging-deploy-piri # piri-0 +``` + +Each pulls the pinned images, recreates changed containers, and waits for healthchecks +(fails the deploy if any service stays unhealthy past the timeout). + +## 6. Register the piri provider with the core (cross-bundle step) + +In local dev, sprue's `post_start.sh` auto-registers piri providers; across bundles that +can't run, so register explicitly once both bundles are healthy: + +```bash +ssh root@23.83.66.244 +cd /root/fil-one/forge/environments/staging/core +PIRI_DID=$(docker compose -p forge-staging-piri exec piri-0 /usr/bin/piri identity parse /keys/piri.pem | grep -oE 'did:key:z[a-zA-Z0-9]+') +docker compose -p forge-staging-core exec sprue \ + sprue client admin provider register "$PIRI_DID" https://piri-0.staging.fil.one /proofs/piri-0-proof.txt +docker compose -p forge-staging-core exec sprue \ + sprue client admin provider weight set "$PIRI_DID" 100 100 +``` + +(The committed `piri-0-proof.txt` must be mounted into the sprue container, or pass it by +path — adjust if needed.) + +## 7. Verify + +```bash +# Health +ssh root@23.83.66.244 'cd /root/fil-one/forge/environments/staging/core && docker compose -p forge-staging-core ps' +ssh root@23.83.66.244 'cd /root/fil-one/forge/environments/staging/piri && docker compose -p forge-staging-piri ps' + +# did:web resolution through Caddy +for h in sprue signing-service delegator; do curl -fsS "https://$h.staging.fil.one/.well-known/did.json" >/dev/null && echo "$h ok"; done + +# End-to-end (note: no mailer in staging — email-based login is unavailable; see Known risks): +# guppy login ... ; SPACE=$(guppy space generate) ; randdir --size 10KB --output /tmp/d +# guppy upload source add $SPACE /tmp/d ; guppy upload $SPACE ; guppy retrieve $SPACE /tmp/out +``` + +## 8. Rollback + +Re-deploy a previous pinned commit: + +```bash +ssh root@23.83.66.244 'cd /root/fil-one/forge && git checkout ' +make staging-deploy-core && make staging-deploy-piri +``` + +Image versions are pinned per bundle in `versions.env`, so rolling back the application +versions is deterministic. Data/schema rollback is out of scope. + +## Known risks / verify-as-you-go + +These are deliberate first-step assumptions to confirm during the manual deploy: + +1. **RPC scheme (http vs ws).** Configs use `http://host.docker.internal:1234/rpc/v1` per the + design doc; the local dev stack used `ws://`. If signing-service/delegator/piri fail to + connect or watch the chain, switch to `ws://host.docker.internal:1234/rpc/v1` (Lotus serves + both on `:1234/rpc/v1`) by editing `LOTUS_RPC_URL` in `environments/staging/smart-contracts.env` + — the single place it's defined — then re-provision and re-deploy. +2. **Container → public-IP hairpin.** Cross-bundle calls and upload→piri callbacks use + `https://*.staging.fil.one`, which resolves to the box's own public IP. If hairpin NAT from + a container to the host's public IP fails, front the calls via `host.docker.internal` HTTP + ports instead. +3. **`no-indexer` config.** sprue runs fine with an empty `indexer.endpoint`. The delegator + *requires* indexing + egress service DIDs and proofs at startup even though neither service + runs — that's why keygen still issues those proofs and the delegator config still references + them. The piri base config keeps non-resolving `[ucan.services.indexer]`/`[publisher]` + sections so `piri init` accepts the config; if piri rejects or misbehaves, remove them. +4. **Auto-created tables/buckets.** The delegator is expected to create its DynamoDB tables; + `minio-init` creates sprue's buckets; sprue runs its own Postgres migrations. If the + delegator does not auto-create tables, create `delegator-allow-list` and + `delegator-provider-info` manually. +5. **No mailer.** staging runs `mailer.type: "nop"`, so no validation emails are sent and + guppy's email-based login step won't complete. End-to-end upload verification therefore + needs an alternative login path (e.g. a pre-provisioned space/account). +6. **Image pinning.** `versions.env` ships rolling `:main` placeholders — pin every application + image to a `@sha256:` digest before a real deploy (`docker buildx imagetools inspect ...`). diff --git a/environments/staging/README.md b/environments/staging/README.md new file mode 100644 index 0000000..5b51587 --- /dev/null +++ b/environments/staging/README.md @@ -0,0 +1,14 @@ +# Forge staging deployment + +Version-pinned Docker Compose manifests and configuration for the Forge **staging** +environment on the Servers.com Calibnet box (`root@23.83.66.244`). The stack is split +into two independently-deployed bundles — [`core/`](core/) (sprue + signing-service + +delegator and their dependency containers) and [`piri/`](piri/) (a piri storage node) — +which talk to each other over public `https://*.staging.fil.one` URLs fronted by the +host's Caddy reverse proxy. Image versions are pinned in each bundle's `versions.env`; +non-secret config lives in committed `config.env` / templates, and secret values come +from 1Password at provision time (never committed). Delegation proofs in [`proofs/`](proofs/) +are generated once by `smelt staging keygen` and committed. + +See **[docs/STAGING_DEPLOY.md](../../docs/STAGING_DEPLOY.md)** for the full fresh-host +bootstrap and deploy runbook. diff --git a/environments/staging/caddy/forge-staging.caddy b/environments/staging/caddy/forge-staging.caddy new file mode 100644 index 0000000..357a06b --- /dev/null +++ b/environments/staging/caddy/forge-staging.caddy @@ -0,0 +1,31 @@ +# Forge staging — Caddy site blocks. +# +# Kept in the repo and shipped to the box under ~/fil-one/forge/caddy/. Import it +# from the existing main Caddyfile (~/storacha/caddy/Caddyfile) with one line: +# +# import /root/fil-one/forge/caddy/*.caddy +# +# then reload: systemctl reload caddy-guppy +# +# Each host terminates TLS (automatic ACME) and reverse-proxies to the bundle's +# loopback-published container port. This also serves did:web resolution: +# https:///.well-known/did.json is proxied straight from the service. +# +# Prerequisite: A records for these hosts must point at the box (23.83.66.244), +# proxied=false — see ../dns/fil-one-staging.tf. + +sprue.staging.fil.one { + reverse_proxy 127.0.0.1:15060 +} + +signing-service.staging.fil.one { + reverse_proxy 127.0.0.1:15030 +} + +delegator.staging.fil.one { + reverse_proxy 127.0.0.1:15040 +} + +piri-0.staging.fil.one { + reverse_proxy 127.0.0.1:15100 +} diff --git a/environments/staging/core/compose.yml b/environments/staging/core/compose.yml new file mode 100644 index 0000000..0f193a3 --- /dev/null +++ b/environments/staging/core/compose.yml @@ -0,0 +1,195 @@ +# Forge staging — CORE bundle. +# +# upload (sprue) + signing-service + delegator, plus their dependency containers +# (postgres, minio, dynamodb-local). Deployed independently of the piri bundle; +# the two talk over public https://*.staging.fil.one URLs (fronted by host Caddy). +# +# Run via the deploy script, which supplies the env files: +# docker compose -p forge-staging-core \ +# --env-file versions.env --env-file config.env --env-file secrets.env \ +# up -d +# +# All published ports bind to 127.0.0.1 (the box's UFW default-denies inbound and +# Docker's iptables can bypass it — loopback binding keeps these off the public +# interface; Caddy fronts them for TLS + did:web). +name: forge-staging-core + +services: + dynamodb-local: + image: ${DYNAMODB_IMAGE} + user: "0:0" + command: ["-jar", "DynamoDBLocal.jar", "-sharedDb", "-dbPath", "/data"] + volumes: + - ${FORGE_DATA_DIR}/dynamodb:/data + healthcheck: + test: ["CMD-SHELL", "bash -c ':>/dev/tcp/127.0.0.1/8000'"] + start_interval: 1s + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + + postgres: + image: ${POSTGRES_IMAGE} + # sprue runs goose migrations on startup; POSTGRES_DB is enough. + environment: + - POSTGRES_USER=sprue + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=sprue + volumes: + - ${FORGE_DATA_DIR}/postgres:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U sprue -d sprue"] + start_interval: 1s + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + + minio: + image: ${MINIO_IMAGE} + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + ports: + - "127.0.0.1:15072:9001" # console (optional, loopback only) + volumes: + - ${FORGE_DATA_DIR}/minio:/data + healthcheck: + test: ["CMD", "mc", "ready", "local"] + start_interval: 1s + interval: 5s + timeout: 3s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + + # Create sprue's buckets once minio is up. Exits 0; sprue waits on completion. + minio-init: + image: ${MC_IMAGE} + depends_on: + minio: + condition: service_healthy + entrypoint: + - /bin/sh + - -c + - | + mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" && + mc mb -p local/agent-message local/delegation local/upload-shards && + echo "buckets ready" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} + restart: "no" + networks: [forge-staging] + + signing-service: + image: ${SIGNER_IMAGE} + user: root + ports: + - "127.0.0.1:15030:7446" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${FORGE_SECRETS_DIR}/payer-key.hex:/keys/payer-key.hex:ro + - ${FORGE_SECRETS_DIR}/signing-service.pem:/keys/signing-service.pem:ro + # signer.yaml carries no secrets and is committed; mounted from the git tree. + - ./config/signer/signer.yaml:/signer.yaml:ro + command: + - "--host" + - "0.0.0.0" + - "--port" + - "7446" + - "--rpc-url" + - "${LOTUS_RPC_URL}" + - "--service-contract-address" + - "${FWSS_ADDRESS}" + - "--signing-key-path" + - "/keys/payer-key.hex" + - "--service-key-file" + - "/keys/signing-service.pem" + - "--service-did" + - "${SIGNING_SERVICE_DID}" + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:7446/healthcheck"] + start_interval: 1s + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + + delegator: + image: ${DELEGATOR_IMAGE} + user: "0:0" + ports: + - "127.0.0.1:15040:80" + command: ["serve", "--host", "0.0.0.0", "--port", "80"] + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${FORGE_SECRETS_DIR}/delegator.pem:/keys/delegator.pem:ro + - ${FORGE_SECRETS_DIR}/delegator.yaml:/.delegator.yaml:ro + # Committed delegation proofs (non-secret) come from the git tree. + - ../proofs:/proofs:ro + depends_on: + dynamodb-local: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/healthcheck"] + start_interval: 1s + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + + sprue: + image: ${SPRUE_IMAGE} + command: ["serve", "--config", "/etc/sprue/config.yaml"] + ports: + - "127.0.0.1:15060:80" + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + # sprue reaches S3/MinIO via the AWS SDK default credential chain. + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} + - SPRUE_SERVER_PUBLIC_URL=${SPRUE_PUBLIC_URL} + volumes: + - ${FORGE_SECRETS_DIR}/sprue.pem:/keys/sprue.pem:ro + - ${FORGE_SECRETS_DIR}/sprue-config.yaml:/etc/sprue/config.yaml:ro + # NOTE: unlike local dev, there is NO post_start provider-registration hook — + # piri lives in a separate bundle. Register piri providers as an explicit + # runbook step after both bundles are healthy (see docs/STAGING_DEPLOY.md). + depends_on: + postgres: + condition: service_healthy + minio: + condition: service_healthy + minio-init: + condition: service_completed_successfully + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:80/health"] + start_interval: 1s + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + restart: unless-stopped + networks: [forge-staging] + +# Persistent data lives on the box's ZFS pool (see FORGE_DATA_DIR in config.env), +# not Docker named volumes — so no top-level `volumes:` block. + +networks: + forge-staging: + name: forge-staging-core diff --git a/environments/staging/core/config.env b/environments/staging/core/config.env new file mode 100644 index 0000000..ed9a2f9 --- /dev/null +++ b/environments/staging/core/config.env @@ -0,0 +1,24 @@ +# Core bundle — non-secret configuration (committed to git). +# +# Consumed by `docker compose --env-file` for variable interpolation in +# compose.yml and (where referenced) by the rendered config templates. No secret +# values belong here — those live in 1Password and are rendered into secrets.env. + +# Host directory holding rendered configs + key files (provisioned via op, never +# committed). Bind-mounted read-only into the containers. +FORGE_SECRETS_DIR=/root/fil-one/forge/secrets + +# Host directory for persistent container data, on the box's large ZFS pool +# (/mnt/data) rather than the root disk where Docker named volumes live. +FORGE_DATA_DIR=/mnt/data/fil-one/forge + +# Chain id, RPC URL, and all contract addresses live in the shared +# environments/staging/smart-contracts.env (single source of truth) — not here. + +# did:web identities (resolved over HTTPS via host Caddy at *.staging.fil.one). +SPRUE_DID=did:web:sprue.staging.fil.one +SIGNING_SERVICE_DID=did:web:signing-service.staging.fil.one +DELEGATOR_DID=did:web:delegator.staging.fil.one + +# Public URLs (Caddy → loopback container ports). +SPRUE_PUBLIC_URL=https://sprue.staging.fil.one diff --git a/environments/staging/core/config/delegator/delegator.yaml.example b/environments/staging/core/config/delegator/delegator.yaml.example new file mode 100644 index 0000000..5b585f3 --- /dev/null +++ b/environments/staging/core/config/delegator/delegator.yaml.example @@ -0,0 +1,32 @@ +# delegator config — STAGING (REDACTED EXAMPLE, structure only). +# The live file is rendered from delegator.yaml.tpl via `op inject` onto the box. + +server: + host: "0.0.0.0" + port: 80 + +store: + region: "us-east-1" + allowlist_table_name: "delegator-allow-list" + providerinfo_table_name: "delegator-provider-info" + providerweight: 1 + endpoint: "http://dynamodb-local:8000" + +delegator: + key_file: "/keys/delegator.pem" + did: "did:web:delegator.staging.fil.one" + indexing_service_web_did: "did:web:indexer.staging.fil.one" + indexing_service_proof_file: "/proofs/indexing-service-proof.txt" + egress_tracking_service_did: "did:web:etracker.staging.fil.one" + egress_tracking_service_proof_file: "/proofs/egress-tracking-proof.txt" + upload_service_did: "did:web:sprue.staging.fil.one" + +contract: + # All of these are rendered from environments/staging/smart-contracts.env at provision time. + chain_client_endpoint: "" + payments_contract_address: "" + service_contract_address: "" + registry_contract_address: "" + transactor: + chain_id: "" + key: "" diff --git a/environments/staging/core/config/delegator/delegator.yaml.tpl b/environments/staging/core/config/delegator/delegator.yaml.tpl new file mode 100644 index 0000000..08b26cc --- /dev/null +++ b/environments/staging/core/config/delegator/delegator.yaml.tpl @@ -0,0 +1,43 @@ +# delegator config — STAGING TEMPLATE. +# +# Rendered with `op inject` into $FORGE_SECRETS_DIR/delegator.yaml and mounted at +# /.delegator.yaml. The only secret is contract.transactor.key (op:// ref). The +# delegator identity is a mounted key file; the delegation proofs are committed +# and mounted from the git tree at /proofs. +# +# Rendered with `op inject` (the transactor key) followed by ${VAR} substitution +# from the shared smart-contracts.env (chain id, RPC URL, contract addresses) — so no +# address is duplicated here. +# +# NOTE: the delegator validates an indexing-service and egress-tracking-service +# delegation at startup even though neither service runs in staging — that's why +# those DIDs + proof files are still required here. + +server: + host: "0.0.0.0" + port: 80 + +store: + region: "us-east-1" + allowlist_table_name: "delegator-allow-list" + providerinfo_table_name: "delegator-provider-info" + providerweight: 1 + endpoint: "http://dynamodb-local:8000" + +delegator: + key_file: "/keys/delegator.pem" + did: "did:web:delegator.staging.fil.one" + indexing_service_web_did: "did:web:indexer.staging.fil.one" + indexing_service_proof_file: "/proofs/indexing-service-proof.txt" + egress_tracking_service_did: "did:web:etracker.staging.fil.one" + egress_tracking_service_proof_file: "/proofs/egress-tracking-proof.txt" + upload_service_did: "did:web:sprue.staging.fil.one" + +contract: + chain_client_endpoint: "${LOTUS_RPC_URL}" + payments_contract_address: "${FILECOIN_PAY_ADDRESS}" + service_contract_address: "${FWSS_ADDRESS}" + registry_contract_address: "${SERVICE_PROVIDER_REGISTRY_ADDRESS}" + transactor: + chain_id: ${CHAIN_ID} + key: "{{ op://Fil One/FilOne Forge Staging/delegator-transactor-key }}" diff --git a/environments/staging/core/config/signer/signer.yaml b/environments/staging/core/config/signer/signer.yaml new file mode 100644 index 0000000..217117e --- /dev/null +++ b/environments/staging/core/config/signer/signer.yaml @@ -0,0 +1,11 @@ +# Signing-service config — STAGING. +# +# Contains no secrets and no contract addresses: the signing key and service +# identity key are mounted as separate files, and the command-line flags in +# compose.yml (--rpc-url, --service-contract-address, --service-did, ...) supply +# and override everything chain-related. Those flags read from the shared +# smart-contracts.env, so there is no address duplicated here. Committed and mounted +# read-only from the git tree. + +host: "0.0.0.0" +port: 7446 diff --git a/environments/staging/core/config/sprue/config.yaml.example b/environments/staging/core/config/sprue/config.yaml.example new file mode 100644 index 0000000..29cfcbc --- /dev/null +++ b/environments/staging/core/config/sprue/config.yaml.example @@ -0,0 +1,40 @@ +# sprue (upload) config — STAGING (REDACTED EXAMPLE, structure only). +# The live file is rendered from config.yaml.tpl via `op inject` onto the box. + +deployment: + environment: "staging" + allow_provision_without_payment_plan: false + max_replicas: 3 + +server: + host: "0.0.0.0" + port: 80 + public_url: "https://sprue.staging.fil.one" + +identity: + key_file: "/keys/sprue.pem" + service_did: "did:web:sprue.staging.fil.one" + +indexer: + endpoint: "" + did: "" + +mailer: + type: "nop" + sender: "noreply@staging.fil.one" + +storage: + type: "postgres" + postgres: + dsn: "postgres://sprue:@postgres:5432/sprue?sslmode=disable" + max_conns: 10 + min_conns: 0 + s3: + endpoint: "http://minio:9000" + region: "us-east-1" + agent_message_bucket: "agent-message" + delegation_bucket: "delegation" + upload_shards_bucket: "upload-shards" + +log: + level: "info" diff --git a/environments/staging/core/config/sprue/config.yaml.tpl b/environments/staging/core/config/sprue/config.yaml.tpl new file mode 100644 index 0000000..343532d --- /dev/null +++ b/environments/staging/core/config/sprue/config.yaml.tpl @@ -0,0 +1,48 @@ +# sprue (upload) config — STAGING TEMPLATE. +# +# Rendered with `op inject` into $FORGE_SECRETS_DIR/sprue-config.yaml and mounted +# at /etc/sprue/config.yaml. Only the Postgres password is a secret (op:// ref); +# everything else is literal. S3/MinIO credentials are supplied via the AWS SDK +# env vars (AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY) set on the container. + +deployment: + environment: "staging" + # Staging enforces payment plans (unlike dev, which bypasses them). + allow_provision_without_payment_plan: false + max_replicas: 3 + +server: + host: "0.0.0.0" + port: 80 # port 80 for did:web resolution; Caddy fronts TLS + public_url: "https://sprue.staging.fil.one" + +identity: + key_file: "/keys/sprue.pem" + service_did: "did:web:sprue.staging.fil.one" + +# No indexer in staging — an empty endpoint disables it (per sprue config docs). +indexer: + endpoint: "" + did: "" + +# No mailer in staging — "nop" drops outgoing mail (so email-based login is +# unavailable; see the runbook). +mailer: + type: "nop" + sender: "noreply@staging.fil.one" + +storage: + type: "postgres" + postgres: + dsn: "postgres://sprue:{{ op://Fil One/FilOne Forge Staging/sprue-postgres-password }}@postgres:5432/sprue?sslmode=disable" + max_conns: 10 + min_conns: 0 + s3: + endpoint: "http://minio:9000" + region: "us-east-1" + agent_message_bucket: "agent-message" + delegation_bucket: "delegation" + upload_shards_bucket: "upload-shards" + +log: + level: "info" diff --git a/environments/staging/core/secrets.env.example b/environments/staging/core/secrets.env.example new file mode 100644 index 0000000..147a4f6 --- /dev/null +++ b/environments/staging/core/secrets.env.example @@ -0,0 +1,10 @@ +# Core bundle — secret environment values (REDACTED EXAMPLE, structure only). +# +# The real file is rendered from secrets.env.tpl via `op inject` and lives only on +# the box. This example shows the shape reviewers should expect — no real values. + +POSTGRES_PASSWORD= +MINIO_ROOT_USER= +MINIO_ROOT_PASSWORD= +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= diff --git a/environments/staging/core/secrets.env.tpl b/environments/staging/core/secrets.env.tpl new file mode 100644 index 0000000..0715e6f --- /dev/null +++ b/environments/staging/core/secrets.env.tpl @@ -0,0 +1,20 @@ +# Core bundle — SECRET environment values (TEMPLATE). +# +# Rendered on a developer machine with: +# op inject -i secrets.env.tpl (streamed to the box, never written locally) +# producing secrets.env in $FORGE_SECRETS_DIR, consumed via +# docker compose --env-file secrets.env +# +# NEVER commit the rendered secrets.env. Only this template (op:// references) is +# tracked. Values resolve from the single 1Password item op://Fil One/FilOne Forge Staging. + +# Postgres (sprue metadata store) — must match the password baked into sprue's DSN. +POSTGRES_PASSWORD={{ op://Fil One/FilOne Forge Staging/sprue-postgres-password }} + +# MinIO root credentials (also used as sprue's S3 access/secret keys). +MINIO_ROOT_USER={{ op://Fil One/FilOne Forge Staging/minio-access-key }} +MINIO_ROOT_PASSWORD={{ op://Fil One/FilOne Forge Staging/minio-secret-key }} + +# sprue reaches S3/MinIO via the AWS SDK default credential chain. +AWS_ACCESS_KEY_ID={{ op://Fil One/FilOne Forge Staging/minio-access-key }} +AWS_SECRET_ACCESS_KEY={{ op://Fil One/FilOne Forge Staging/minio-secret-key }} diff --git a/environments/staging/core/versions.env b/environments/staging/core/versions.env new file mode 100644 index 0000000..8d53e8b --- /dev/null +++ b/environments/staging/core/versions.env @@ -0,0 +1,20 @@ +# Core bundle — pinned image versions (THE atomic deploy unit). +# +# Bump these to roll the staging core stack forward; revert the commit to roll +# back. Image references MUST be immutable: pin every application image to a +# @sha256:... digest before a real deploy. The rolling :main tags below are +# placeholders so `docker compose config` parses — replace them. +# +# Resolve a digest with: +# docker buildx imagetools inspect ghcr.io/fil-forge/sprue:main +# +# Application services (pin to digests): +SPRUE_IMAGE=ghcr.io/fil-forge/sprue:main +SIGNER_IMAGE=ghcr.io/fil-forge/piri-signing-service:main +DELEGATOR_IMAGE=ghcr.io/fil-forge/delegator:main + +# Dependency containers (a pinned tag is acceptable per the design; digests preferred): +POSTGRES_IMAGE=postgres:16-alpine +MINIO_IMAGE=minio/minio:RELEASE.2024-10-13T13-34-11Z +MC_IMAGE=minio/mc:RELEASE.2024-10-08T09-37-26Z +DYNAMODB_IMAGE=amazon/dynamodb-local:2.5.2 diff --git a/environments/staging/dns/fil-one-staging.tf b/environments/staging/dns/fil-one-staging.tf new file mode 100644 index 0000000..0b2803e --- /dev/null +++ b/environments/staging/dns/fil-one-staging.tf @@ -0,0 +1,32 @@ +# Forge staging DNS — REFERENCE COPY. +# +# This is not applied from the smelt repo. Copy these resource definitions into +# fil-one/infrastructure → environments/staging/fil-one.tf +# which already defines `local.zone_id` (data.cloudflare_zone.filone, "fil.one"). +# +# A records for the Forge staging services fronted by host Caddy on the +# Servers.com Calibnet box (root@23.83.66.244). proxied = false (DNS-only) so +# Caddy's ACME HTTP-01 challenge reaches the host directly and did:web resolution +# (https:///.well-known/did.json) sees the real origin. + +locals { + forge_staging_box_ipv4 = "23.83.66.244" + + #