Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -240,9 +240,10 @@ aws --profile smelt s3 cp README.md s3://my-bucket/

`scripts/s3-key.sh` creates the hilt tenant if needed, mints an access key with every S3
permission, and writes the profile with the stack's region, ingot's endpoint and path-style
addressing. `TENANT=` / `PROFILE=` override the defaults. Rerun it after `make down && make up`:
hilt-vault is in-memory, so the tenant's signing key is gone and the script moves on to the
next free tenant id (`dev-2`, ...) with a fresh key.
addressing. `TENANT=` / `PROFILE=` override the defaults. hilt-vault keeps the tenant's
signing key on a volume, so the profile keeps working across `make down && make up`. A stack
whose vault has lost the key (one restored from a snapshot saved before the vault persisted)
makes the script move on to the next free tenant id (`dev-2`, ...) with a fresh key.

### Regenerating Keys and Proofs

Expand Down Expand Up @@ -329,7 +330,7 @@ All host-side ports live in a dedicated `15XXX` range to avoid collision with co
| piri-{N} | 15100 + N | HTTP/UCAN | Storage node(s); N defined by `smelt.yml` (default 1, max 9) |
| hilt | 15110 | HTTP/UCAN | Tenant management (Tenant API + UCAN RPC) |
| hilt-postgres | 15111 | PostgreSQL | Hilt tenant/provider store |
| hilt-vault | 15112 | HTTP | Hilt key vault (OpenBao dev mode) |
| hilt-vault | 15112 | HTTP | Hilt key vault (OpenBao, raft storage) |
| plc | 15120 | HTTP | did:plc directory (reference implementation) |
| plc-postgres | 15121 | PostgreSQL | did:plc directory store |
| ingot | 15130 | S3/HTTP | S3 gateway over Forge |
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ All host ports live in a dedicated `15XXX` range to avoid collision with common
| piri-{N} | 15100+N | Storage node(s) with PDP proofs; N declared in `smelt.yml` (default 1, max 9) |
| hilt | 15110 | Tenant management (Tenant API + UCAN RPC) |
| hilt-postgres | 15111 | Postgres for hilt's tenant/provider store |
| hilt-vault | 15112 | Hilt key vault (OpenBao in dev mode) |
| hilt-vault | 15112 | Hilt key vault (OpenBao, raft storage) |
| plc | 15120 | did:plc directory (reference implementation) |
| plc-postgres | 15121 | Postgres for the did:plc directory |
| ingot | 15130 | S3 gateway over Forge |
Expand Down
13 changes: 8 additions & 5 deletions docs/PERF_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,9 @@ LABEL=after ./scripts/perf-s3-speedtest.sh run

`setup` mints a key for tenant `perf` (AWS CLI profile `smelt-perf`, the same
way `make s3-key` does), creates the bucket and generates the test files in
`generated/perf/testfiles`. Re-run it after `make down && make up`: hilt's dev
vault is in memory, so the old key stops working. `setup` then moves to the
`generated/perf/testfiles`. The key survives `make down && make up`; re-run
`setup` after `make clean` or a snapshot restore. If hilt has the tenant but
not its key (a snapshot saved before the vault persisted), `setup` moves to the
next free tenant (`perf-2`, ...) and creates that tenant's bucket
(`perf-s3-speedtest-perf-2`), since ingot bucket names are global and the old
one belongs to `perf`.
Expand All @@ -153,7 +154,8 @@ The `large` set writes 75 GiB per run, so plan for about 200 GB of free disk
per run and 400 GB for a before/after pair.

`SNAPSHOT` runs `make down && make up SNAPSHOT=...` and then `setup` again,
because the restore loses hilt's access key the same way a restart does. An
because the restore returns hilt to the snapshot's state, which holds only the
keys that existed when it was saved. An
exported `SMELT_MANIFEST` takes precedence over the snapshot's own manifest,
so the script refuses to restore when the two differ.

Expand Down Expand Up @@ -196,8 +198,9 @@ LABEL=after ./scripts/perf-drill.sh run
`setup` mints a key for tenant `drill` (AWS CLI profile `smelt-drill`, the
same way `make s3-key` does) and writes it with ingot's endpoint and region to
`generated/perf-runs/drill/provider/.env`, readable only by you. Neither key
is printed. Re-run `setup` after `make down && make up`: hilt's dev vault is
in memory, so the old key stops working and `setup` moves to tenant `drill-2`.
is printed. The key survives `make down && make up`. If hilt has the tenant but
not its key (a snapshot saved before the vault persisted), `setup` moves to
tenant `drill-2`.
Re-run it after `make clean` too, which deletes the tenant along with its key;
`setup` then creates tenant `drill` again. Without it, `run` stops at its
smoke check with `InvalidAccessKeyId`.
Expand Down
2 changes: 2 additions & 0 deletions docs/SNAPSHOTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ generated/snapshots/<name>/
├── guppy-data.tar.gz # client login + spaces
├── swarf-postgres-data.tar.gz # revocation records
├── hilt-postgres-data.tar.gz # tenant state
├── hilt-vault-data.tar.gz # tenant/access-key private keys (OpenBao raft storage)
├── hilt-vault-init.tar.gz # its unseal share (dev-only custody)
├── plc-postgres-data.tar.gz # DID PLC registry
├── ingot-data.tar.gz # ingot node state
├── ingot-postgres-data.tar.gz # ingot registry
Expand Down
2 changes: 2 additions & 0 deletions embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import "embed"
//go:embed compose.yml .env

//go:embed systems/common/compose.yml
//go:embed systems/common/openbao/*

//go:embed systems/blockchain/compose.yml
//go:embed systems/blockchain/state/deployed-addresses.json
Expand All @@ -22,6 +23,7 @@ import "embed"

//go:embed systems/hilt/compose.yml
//go:embed systems/hilt/register-provider.sh
//go:embed systems/hilt/openbao/*
Comment thread
alanshaw marked this conversation as resolved.

//go:embed systems/ingot/compose.yml
//go:embed systems/ingot/config/*
Expand Down
5 changes: 3 additions & 2 deletions images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ var thirdPartyImages = map[string]map[string]string{
"postgres": "${POSTGRES_IMAGE:-postgres:16-alpine}",
},
"systems/hilt/compose.yml": {
"hilt-vault": "${OPENBAO_IMAGE:-openbao/openbao:2.6}",
"hilt-postgres": "${POSTGRES_IMAGE:-postgres:16-alpine}",
"hilt-vault": "${OPENBAO_IMAGE:-openbao/openbao:2.6}",
"hilt-vault-init": "${OPENBAO_IMAGE:-openbao/openbao:2.6}",
"hilt-postgres": "${POSTGRES_IMAGE:-postgres:16-alpine}",
},
"systems/swarf/compose.yml": {
"swarf-postgres": "${POSTGRES_IMAGE:-postgres:16-alpine}",
Expand Down
2 changes: 2 additions & 0 deletions pkg/snapshot/volumes.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ func resolveVolumes(m *manifest.Manifest) ([]string, error) {
"guppy-data", // systems/guppy — client's login/space state
"swarf-postgres-data", // systems/swarf — revocation records
"hilt-postgres-data", // systems/hilt — tenant state
"hilt-vault-data", // systems/hilt — tenant/access-key private keys (raft storage)
"hilt-vault-init", // systems/hilt — unseal share for the above; restored together
Comment thread
alanshaw marked this conversation as resolved.
"plc-postgres-data", // systems/plc — DID PLC registry
"ingot-data", // systems/ingot — node state
"ingot-postgres-data", // systems/ingot — registry
Expand Down
3 changes: 3 additions & 0 deletions pkg/stack/stack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ func TestEmbeddedFilesExist(t *testing.T) {
"systems/upload/register-providers.sh",
"systems/ingot/openbao/config.hcl",
"systems/ingot/openbao/init.sh",
"systems/hilt/openbao/config.hcl",
"systems/hilt/openbao/init.sh",
"systems/common/openbao/bootstrap.sh",
}

for _, f := range files {
Expand Down
2 changes: 1 addition & 1 deletion scripts/perf-drill.sh
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ smoke_fail() {
$err
The drill would fail the same way. Check the stack's logs: docker compose logs ingot piri-0
InvalidAccessKeyId means hilt no longer has the key, as after 'make clean' or a
restart; '$0 setup' mints a new one.
snapshot restore; '$0 setup' mints a new one.
If piri reports a signature mismatch, the keys and proofs in generated/ are likely out of
sync; 'make regen', then 'make clean && make up' and '$0 setup' regenerates both."
}
Expand Down
7 changes: 4 additions & 3 deletions scripts/perf-s3-speedtest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
# LABEL run label, required for `run`
# RUNS repeats per file (default 1)
# SNAPSHOT run: `make down && make up SNAPSHOT=...` first, then re-run setup
# (hilt's dev vault is in memory, so a restore loses the access key).
# (the restore returns hilt to the snapshot's keys, so the access key goes).
# With SMELT_MANIFEST also set, the two manifests must match.
# PERF_EXTRA_METADATA a JSON object recorded verbatim as `extra` in metadata.json
# and runs.jsonl; anything else stops the run before it starts
Expand All @@ -39,8 +39,9 @@ RUNS="${RUNS:-1}"
TENANT=perf
PROFILE=smelt-perf
# The bucket is named after the tenant the profile ended up on (see
# perf_bucket): ingot bucket names are global, and after a restart s3-key.sh
# may move to perf-2, which cannot reuse the bucket perf created.
# perf_bucket): ingot bucket names are global, and when hilt has lost the
# tenant's key s3-key.sh moves to perf-2, which cannot reuse the bucket perf
# created.
BUCKET_PREFIX=perf-s3-speedtest
TARGETS_TEMPLATE="$PROJECT/generated/perf/s3_targets.ini"
# Containers whose CPU/memory and logs are captured around each run.
Expand Down
19 changes: 10 additions & 9 deletions scripts/s3-key.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@
# returns it once); this script never prints it. The profile also carries a
# custom `tenant_id` key naming the tenant it belongs to.
#
# After `make down && make up` hilt has forgotten the tenant's signing key
# (hilt-vault is in-memory), so the script moves on to the next free tenant id
# (dev-2, dev-3, ...) and the previously saved key stops working; rerun
# `make s3-key` to get a working profile again.
# hilt-vault keeps the tenant's signing key on a volume, so a saved profile
# keeps working across `make down && make up`. If the vault has lost the key
# (a stack restored from a snapshot saved before the vault persisted), the
# script moves on to the next free tenant id (dev-2, dev-3, ...) and the
# previously saved key stops working; rerun `make s3-key` for a working profile.
#
# Needs: a running stack, docker, curl, jq, and AWS CLI v2 (2.13+, for the per-profile
# endpoint_url setting).
Expand Down Expand Up @@ -95,10 +96,10 @@ mint_key() {
}

# Hilt keeps tenant records in postgres but the tenant's signing key in
# hilt-vault, which runs OpenBao in dev mode: in memory, gone after
# `make down && make up`. The tenant then still exists (PUT says 200) but
# every access-key request fails with 500, and so does DELETE, which needs
# the same key to sign the did:plc tombstone. Nothing can revive that
# hilt-vault. If the vault has lost the key (a stack restored from a snapshot
# saved before the vault persisted), the tenant still exists (PUT says 200)
# but every access-key request fails with 500, and so does DELETE, which
# needs the same key to sign the did:plc tombstone. Nothing can revive that
# tenant, so fall back to the next free id (dev-2, dev-3, ...).
tenant="$TENANT"
status=
Expand All @@ -107,7 +108,7 @@ for attempt in $(seq 2 9); do
status="$(mint_key "$tenant")"
if [ "$status" = "200" ] || [ "$status" = "201" ]; then break; fi
if [ "$status" = "500" ] && [ "$state" = "exists" ]; then
echo "tenant $tenant exists but hilt cannot sign for it (hilt-vault lost its key on restart); trying $TENANT-$attempt" >&2
echo "tenant $tenant exists but hilt cannot sign for it (hilt-vault has lost its key); trying $TENANT-$attempt" >&2
tenant="$TENANT-$attempt"
status=
continue
Expand Down
116 changes: 116 additions & 0 deletions systems/common/openbao/bootstrap.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
#!/bin/sh
# Shared first half of an OpenBao init job, sourced by each system's
# openbao/init.sh. It brings a non-dev `bao server` with raft storage to a
# usable state, idempotently, so the init job can run on every `make up`:
#
# 1. first boot: `bao operator init` (one unseal share); the share and the
# root token are kept on the init volume (/init). Dev-only custody:
# production replaces the stored share with a transit seal against a
# central OpenBao.
# 2. every boot: unseal if sealed, then wait for the node to go active
# (raft serves nothing until it has a leader).
#
# On return BAO_TOKEN holds the root token, for the caller's provisioning.
# The caller sets BAO_ADDR and OPENBAO_NAME (the server's compose service
# name, used in log lines and error hints).
#
# Nothing secret is ever echoed: no `set -x`, and bao output that carries
# key material goes to files or /dev/null.

: "${BAO_ADDR:?BAO_ADDR must point at the OpenBao server}"
: "${OPENBAO_NAME:?OPENBAO_NAME (the compose service name of the server) must be set}"

INIT_DIR=/init
UNSEAL_KEY_FILE="$INIT_DIR/unseal-key"
ROOT_TOKEN_FILE="$INIT_DIR/root-token"

log() { echo "$OPENBAO_NAME-init: $*"; }
die() { echo "$OPENBAO_NAME-init: $*" >&2; exit 1; }

# Server state comes from exit codes, which are stable across output formats.
# Documented codes:
# bao status 0 unsealed, 1 error, 2 sealed
# https://openbao.org/docs/commands/status/
# bao operator init -status 0 initialized, 1 error, 2 not initialized
# https://openbao.org/docs/commands/operator/init/
# Two cases the docs leave implicit, confirmed against openbao/openbao:2.6:
# an uninitialized server is also sealed, so `bao status` exits 2 for it, and
# `operator init -status` exits 2 (not 1) when the server is unreachable. So
# reachability is settled first via `bao status`, then initialization, then
# the seal.
is_initialized() { bao operator init -status >/dev/null 2>&1; }
is_sealed() {
rc=0
bao status >/dev/null 2>&1 || rc=$?
[ "$rc" -eq 2 ]
}

log "waiting for OpenBao at $BAO_ADDR..."
waited=0
while :; do
rc=0
bao status >/dev/null 2>&1 || rc=$?
if [ "$rc" -ne 1 ]; then break; fi
if [ "$waited" -ge 120 ]; then
die "OpenBao never answered after ${waited}s; aborting"
fi
sleep 1
waited=$((waited + 1))
done
log "OpenBao is answering (took ${waited}s)"

# json_string JSON KEY prints the string value of KEY from JSON, for a value
# that is either a plain string or the first element of an array. Whitespace
# is stripped first so pretty-printed and compact output parse the same;
# base64 and token values contain no whitespace or quotes.
json_string() {
printf '%s' "$1" | tr -d ' \n\r\t' | sed -n "s/.*\"$2\":\[\{0,1\}\"\([^\"]*\)\".*/\1/p"
}

mkdir -p "$INIT_DIR"

if ! is_initialized; then
log "server is uninitialized; running operator init (1 share)"
# Stale material from a previous data volume is useless now; overwrite.
init_json=$(bao operator init -key-shares=1 -key-threshold=1 -format=json)
unseal_key=$(json_string "$init_json" unseal_keys_b64)
root_token=$(json_string "$init_json" root_token)
[ -n "$unseal_key" ] || die "could not parse the unseal key from operator init output"
[ -n "$root_token" ] || die "could not parse the root token from operator init output"
umask 077
printf '%s' "$unseal_key" > "$UNSEAL_KEY_FILE"
printf '%s' "$root_token" > "$ROOT_TOKEN_FILE"
unset init_json unseal_key root_token
log "initialized; unseal share and root token stored on the init volume"
elif [ ! -s "$UNSEAL_KEY_FILE" ] || [ ! -s "$ROOT_TOKEN_FILE" ]; then
die "server is initialized but $INIT_DIR holds no unseal material (init volume lost); run 'make clean' to reset both $OPENBAO_NAME volumes"
fi

if is_sealed; then
log "unsealing"
bao operator unseal "$(cat "$UNSEAL_KEY_FILE")" >/dev/null
fi
is_sealed && die "server is still sealed after unseal"
log "unsealed"

# An unsealed raft node is not yet serving: it has to win its leadership
# election first, and until it does every request is refused with 500
# "local node not active but active cluster node not found". sys/health's
# default codes settle it, since 200 means initialized, unsealed and active
# (a standby answers 429, a sealed node 503) — unlike the compose
# healthcheck, which passes sealedcode and uninitcode so it can gate the init
# job on a server that is merely listening.
# -T bounds each probe, so one hung connect cannot outlast the deadline, which
# is wall-clock rather than a count of attempts.
log "waiting for the node to become active..."
started=$(date +%s)
until wget -q -T 2 --spider "$BAO_ADDR/v1/sys/health" 2>/dev/null; do
if [ "$(($(date +%s) - started))" -ge 60 ]; then
die "node never became active after 60s; aborting"
fi
sleep 1
done
log "node is active (took $(($(date +%s) - started))s)"

BAO_TOKEN=$(cat "$ROOT_TOKEN_FILE")
export BAO_TOKEN
23 changes: 15 additions & 8 deletions systems/hilt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,12 @@ the tenant as a customer with the upload service via `/customer/add`.
- **hilt** - Tenant management service (`ghcr.io/fil-forge/hilt:main`)
- **hilt-postgres** - PostgreSQL for hilt's tenant/provider/access-key stores
(goose migrations run at hilt startup)
- **hilt-vault** - OpenBao in dev mode for tenant/access-key private keys
(KV v2 at the `secret` mount)
- **hilt-vault** - OpenBao for tenant/access-key private keys (server mode,
raft storage on `hilt-vault-data`, KV v2 at the `secret` mount)
- **hilt-vault-init** - one-shot: initializes and unseals `hilt-vault`, then
provisions the KV mount, hilt's policy and hilt's scoped token
(`openbao/init.sh`, with the shared bootstrap in
`../common/openbao/bootstrap.sh`)

## Ports

Expand All @@ -41,11 +45,11 @@ All configuration is via `HILT_*` environment variables in `compose.yml`:
only; never use a production key here.
- Storage: postgres via the `hilt-postgres` sidecar (dev-only `hilt:hilt`
credentials; data persists in the `hilt-postgres-data` volume).
- Vault: OpenBao dev mode via `hilt-vault` (`HILT_VAULT_TYPE=openbao`),
token auth with the dev root token (`HILT_VAULT_TOKEN`, default
`dev-root-token` — local dev only). Keys survive hilt restarts, but
dev-mode OpenBao stores in memory, so a `hilt-vault` container restart
clears them.
- Vault: OpenBao via `hilt-vault` (`HILT_VAULT_TYPE=openbao`), token auth
with a scoped token that `hilt-vault-init` provisions on every boot
(`HILT_VAULT_TOKEN`, default `dev-hilt-vault-token` — local dev only).
Keys persist in the `hilt-vault-data` volume, alongside the records in
`hilt-postgres-data`; `make clean` resets both.
- PLC directory: the local reference server at `http://plc:3000`.
- Upload service: `did:web:upload` at `http://upload:80`, presenting the
`upload → hilt` `/customer/add` delegation from
Expand Down Expand Up @@ -78,14 +82,17 @@ Registration is idempotent — when the record already exists in postgres the
## Volumes

- `hilt-postgres-data` - Hilt's tenant/provider/access-key records
- `hilt-vault-data` - OpenBao raft storage: tenant and access-key private keys
- `hilt-vault-init` - hilt-vault's unseal share and root token (dev-only
custody)

## Dependencies

- plc (service_healthy)
- upload (service_healthy)
- swarf (service_healthy)
- hilt-postgres (service_healthy)
- hilt-vault (service_healthy)
- hilt-vault-init (service_completed_successfully)

## Requirements and Notes

Expand Down
Loading
Loading