Skip to content

Stand Up the Directory

What this is: deploying the Directory — the identity authority that issues every IdentityToken and signing permit in the system. Nothing else can work until this exists: servers, users, and web clients all verify their tokens against the Directory's public signing key. When you'd do it: first deployment of the system, or rebuilding the Directory from scratch. How long it takes: provisioning and DNS is an hour or two; the first-admin /setup flow takes about five minutes once the site is reachable.

This is the first thing you stand up — before servers, before onboarding any users. See the system overview and PKI model for how the Directory fits into the trust chain.

Who can do this: a deployment engineer provisions the infrastructure. The first-admin bootstrap is part of the same job — because there are no Admins yet, this step creates the very first one via the one-time /setup flow (not the normal admin dashboard, which doesn't exist yet). Both halves are the engineer's job on day one.

The shape of it

flowchart LR
    A["Provision the<br/>Directory VM<br/>(+ Cloud SQL DB)"] --> B["DNS resolves;<br/>Caddy provisions<br/>TLS"]
    B --> C["Open /setup<br/>Enter the setup key"]
    C --> D["First person registers<br/>their FIDO passkey"]
    D --> E["They become<br/>the first Admin"]
    E --> F["Directory is live —<br/>sign in and start<br/>adding servers + users"]

Before you start

  • Access to the GCP project and permission to run OpenTofu.
  • A DNS record pointing the Directory hostname at the static IP Terraform will create (Caddy can't provision TLS until DNS resolves).
  • A FIDO2 hardware security key for the first Admin — this is the only authentication method the admin UI accepts.
  • The global Terraform stack already applied (terraform/global/) — the Directory's deploy service account is an output of that stack.

Step 1 — Provision the Directory

The Directory stack is in the infrastructure repo at terraform/environments/auth/. It provisions a Container-Optimized OS VM running three containers: the Directory app, Caddy (TLS termination and reverse proxy), and the Cloud SQL Auth Proxy sidecar. The database is a Cloud SQL Postgres 17 instance, also provisioned by the same stack.

cd terraform/environments/auth
cp terraform.tfvars.example terraform.tfvars   # fill in your GCP project ID
tofu init && tofu apply

The stack creates:

  • the Directory VM (default machine type e2-small, zone europe-west2-a)
  • a static IP that becomes auth.bedrockdefence.com (or your configured hostname)
  • a Cloud SQL PG17 instance (default tier db-custom-1-3840)
  • all Secret Manager secret containers; tofu apply also bootstraps pg_cron and schedules the cleanup_registered_keys maintenance job on Cloud SQL

Secrets (APP_KEY, db-password, cloudsql-postgres) are auto-generated by Terraform and stored in Secret Manager — you don't set them by hand.

After apply, point DNS to the static IP from tofu output. Caddy won't provision its Let's Encrypt certificate until DNS resolves.

One thing you do need to supply manually: the Directory signing key (DIRECTORY_SIGNING_KEY_PATH). This is the Ed25519 key the Directory uses to sign every IdentityToken. Back it up before anything else — losing it means re-issuing all tokens. Check the infrastructure repo README for how it is stored and uploaded.

Docker lab path (non-production)

If you're running a local lab, the directory repo has a docker-compose.yml that starts a Directory container alongside a plain Postgres 17 container:

cp .env.example .env
# Fill in APP_KEY (run: node ace generate:key) and DIRECTORY_SIGNING_KEY_PATH
docker compose up --build

The entrypoint runs migrations automatically on startup. The Directory starts on port 3333. This is useful for development and testing — it's not the production path.

Step 2 — Run the first-time /setup

This step only works once. The /setup route checks whether any principals exist in the database. When the database is empty, setup is open. The moment the first Admin is created, the setup key is consumed and /setup returns 404 for every future request.

How the setup key works: on boot, when no principals exist, the Directory generates a random 32-byte setup key (displayed as 64 uppercase hex characters) and prints it to the application log. It is valid for 30 minutes. If it expires before you use it, restart the process (or container) to mint a fresh one.

  1. Tail the logs to find the setup key:
  2. Cloud path: gcloud compute instances get-serial-port-output bedrock-directory-auth --zone=europe-west2-a | grep -i setup
  3. Docker path: docker compose logs directory | grep -i setup

  4. Open https://<your-directory-hostname>/setup in a browser.

  5. Enter the setup key from the logs and your display name. This calls POST /setup/fido/begin, which validates the key and starts a WebAuthn registration ceremony.

  6. Register your FIDO2 passkey when the browser prompts you. On success the browser calls POST /setup/fido/complete, the Directory creates the first principal with roles admin and operator, and the setup key is consumed (activeSetupKey is set to null).

  7. You're redirected to /login. From this point /setup is permanently closed — it returns 404 because principals now exist.

The three setup routes (confirmed in start/routes.ts):

Route What it does
GET /setup Shows the setup page (404 if principals exist)
POST /setup/fido/begin Validates the setup key, starts FIDO registration
POST /setup/fido/complete Verifies the passkey, creates the first Admin

Step 3 — Sign in as the first Admin

Navigate to /login and authenticate with the passkey you just registered. You land on the admin dashboard at /admin/dashboard. From here you can:

  • add more principals (people) and devices
  • register servers (each server must be enrolled with the Directory before it can carry traffic — see Add a server)
  • manage signing keys via /admin/keys

How to know it worked

  • The admin dashboard loads at /admin/dashboard and you can sign in with your passkey.
  • GET /api/.well-known/directory-key returns the Directory's public signing key (lowercase hex) — this is the endpoint everything else uses to verify IdentityTokens.
  • The serial console (cloud path) shows migrations completed and no startup errors.

If something goes wrong

  • /setup returns 404 immediately. A principal already exists — someone ran setup before you, or a previous attempt partly succeeded. Check whether an Admin already exists by inspecting the principals table directly, then either sign in as that Admin or follow the documented reset procedure in the directory repo.
  • Setup key rejected ("Invalid or expired setup key"). The key is 30-minute-lived and in-memory only. Restart the Directory process/container to generate a fresh one, then return to /setup immediately.
  • Database not reachable. On the cloud path the Cloud SQL Auth Proxy sidecar handles the connection — check that the VM started cleanly and that the Cloud SQL instance is running. On Docker, check that the db container is healthy before the directory container started (the entrypoint has a fixed 5-second wait; increase it if needed).
  • Signing key missing or wrong path. The Directory won't start without a valid DIRECTORY_SIGNING_KEY_PATH. Confirm the key file exists at the configured path (cloud path: uploaded to Secret Manager and fetched by the startup script; Docker: mounted via the volume or .env). Back this file up — it cannot be recovered if lost.
  • Caddy won't provision TLS / HTTPS not working. DNS must resolve to the static IP before Let's Encrypt will issue a certificate. Confirm the A record is live and allow a few minutes for propagation.

See also

  • Operator training index
  • Stand up a deployment — the Directory is step 2 in the full sequence.
  • Set up the certificate chain (PKI) — the trust hierarchy the Directory signs tokens under.
  • Add a server — once the Directory is live, the next step.
  • PKI model — how IdentityTokens fit into the wider trust chain.
  • The infrastructure repo — terraform/environments/auth/ for the full cloud provisioning walkthrough.
  • The directory repo — README.md for the full env-var reference and docker-compose.yml for the local lab path.

Verified against directory@e8287cd on 2026-06-07 — first-run flow: app/domains/setup/ + start/routes.ts (/setup); provisioning: infrastructure repo terraform/environments/auth/.