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
/setupflow (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¶
Cloud path (recommended)¶
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, zoneeurope-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 applyalso bootstrapspg_cronand schedules thecleanup_registered_keysmaintenance 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.
- Tail the logs to find the setup key:
- Cloud path:
gcloud compute instances get-serial-port-output bedrock-directory-auth --zone=europe-west2-a | grep -i setup -
Docker path:
docker compose logs directory | grep -i setup -
Open
https://<your-directory-hostname>/setupin a browser. -
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. -
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 rolesadminandoperator, and the setup key is consumed (activeSetupKeyis set tonull). -
You're redirected to
/login. From this point/setupis 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/dashboardand you can sign in with your passkey. GET /api/.well-known/directory-keyreturns 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¶
/setupreturns 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 theprincipalstable directly, then either sign in as that Admin or follow the documented reset procedure in thedirectoryrepo.- 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
/setupimmediately. - 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
dbcontainer is healthy before thedirectorycontainer 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
infrastructurerepo —terraform/environments/auth/for the full cloud provisioning walkthrough. - The
directoryrepo —README.mdfor the full env-var reference anddocker-compose.ymlfor 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/.