You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(docker): give a preview its own login apps and signing key
A preview starts from a clone of another instance's database and reads it
with that instance's encryption key, because that is the only key those
rows can be read with. The same key unseals the source's JWT signing key
and decrypts its OAuth client secrets, so a preview inherited both.
Sign-in could therefore never work: the cloned OAuth apps are registered
against the source's hostname, and a provider rejects every authorization
request that starts from a preview host. The preview also minted its own
tokens signed with the source instance's production signing key.
The clone is now re-homed before the application server starts. Emptying
login_provider, jwt_signing_key and issued_jwt makes the preview rebuild
each from its own configuration on boot. Accounts are untouched, because
identity_link keys on identity_provider rather than on login_provider.
The policy moves out of a shell heredoc into sanitize.sql, organised by
the two questions it answers — a clone must not act, and a clone must not
keep the source instance's identity — so it can be read and diffed as SQL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EZADQeSx6zQNNqsdu7AAqZ
Pull request previews now sign in through their own login apps instead of the ones they cloned. A preview starts from a copy of another instance's database, and it used to keep that instance's OAuth registrations — whose callback URLs belong to the original hostname, so every sign-in attempt was rejected by the provider before it began. A preview now rebuilds its login providers from its own configuration on first boot, and mints its own token-signing key rather than reusing the cloned one. Existing accounts are unaffected: signing in through the preview's own app lands on the same account as before.
Copy file name to clipboardExpand all lines: docker/preview/README.md
+35-5Lines changed: 35 additions & 5 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -22,9 +22,13 @@ Before the first restore, the seeder recreates only the uniquely resolved previe
22
22
both succeed. A failed or partial first attempt can therefore retry cleanly, while redeploying an
23
23
already seeded PR preserves changes made while testing. A new preview volume gets a fresh staging clone.
24
24
25
-
## Silence policy
25
+
## Preview policy
26
26
27
-
Before the application server starts, `seed-loader`:
27
+
`seed-loader` applies [`sanitize.sql`](./sanitize.sql) to the restored clone before the application
28
+
server starts. A clone is another instance's live database, so the policy answers two questions, and
29
+
the file is organised by them.
30
+
31
+
### Silence — a clone must not act
28
32
29
33
1. disables automatic and manual practice-review triggers for every workspace;
30
34
2. disables each `PRACTICE_REVIEW` model binding while preserving its selected model;
@@ -37,6 +41,29 @@ visible in the UI. To test reviews for one workspace, enable its practice-review
37
41
enable the desired manual or automatic trigger in that workspace's practice-review settings. Those
38
42
changes persist for the lifetime of the PR preview.
39
43
44
+
### Re-home — a clone must not keep the source instance's identity
45
+
46
+
A preview runs with the **source instance's**`HEPHAESTUS_SECURITY_ENCRYPTION_KEY`, because that is the
47
+
only key the cloned rows can be read with. Without it the preview boots and then fails every request
48
+
that touches a credential, with `AEADBadTagException: Tag mismatch` in the log. That key also unseals
49
+
the source's JWT signing key and decrypts its OAuth client secrets, so three tables are emptied and
50
+
rebuilt from this deployment's own configuration:
51
+
52
+
| Table | Why it cannot be inherited |
53
+
|---|---|
54
+
|`login_provider`| The source's OAuth apps are registered against the source's hostname, so the provider rejects every sign-in that starts from a preview host |
55
+
|`jwt_signing_key`| The preview would otherwise mint its own tokens signed with the source instance's production signing key |
56
+
|`issued_jwt`| Cloned sessions belong to the source instance's users |
57
+
58
+
`LoginProviderService` seeds `login_provider` from the environment whenever a registration id is
59
+
absent, so emptying the table hands the preview its own login apps on the next boot — which is why the
60
+
preview stack needs `GITHUB_OAUTH_*` (and any other provider it should offer) pointed at an OAuth app
61
+
whose callback covers the preview hostnames. A provider with no credentials in the preview environment
62
+
is simply not offered; Slack, being link-only, is normally absent for that reason.
63
+
64
+
Accounts survive all of this: `identity_link` keys on `identity_provider`, not on `login_provider`, so
65
+
a cloned user signs in through the preview's own OAuth app and lands on the same account.
66
+
40
67
## One-time server and Coolify setup
41
68
42
69
1. Keep staging's `nats-server` attached to the external Docker network `shared-network`.
@@ -45,11 +72,14 @@ changes persist for the lifetime of the PR preview.
45
72
joins `shared-network` as a second network.
46
73
4. Set `PREVIEW_SEED_SOURCE_CONTAINER=app-postgres-1` if the staging Compose project/container name
47
74
ever changes. The source user defaults to `root` and database to `hephaestus`.
48
-
5. Assign the web and API services sibling wildcard domains. With the current template these are
75
+
5. Set `HEPHAESTUS_SECURITY_ENCRYPTION_KEY` to the **seed source instance's** key, and point
76
+
`GITHUB_OAUTH_CLIENT_ID`/`_SECRET` at an OAuth app whose callback covers the preview hostnames —
77
+
see the re-home policy above for both.
78
+
6. Assign the web and API services sibling wildcard domains. With the current template these are
49
79
`pr<id>.hephaestus.felixdietrich.com` and `pr<id>.api.hephaestus.felixdietrich.com`.
50
-
6. Leave the preview `IMAGE_TAG` unset. Coolify injects `SOURCE_COMMIT`, and CI publishes the matching
80
+
7. Leave the preview `IMAGE_TAG` unset. Coolify injects `SOURCE_COMMIT`, and CI publishes the matching
51
81
application-server image before the preview update is requested.
52
-
7. Put every preview-safe value in the application's **base** environment variables, not in Coolify's
82
+
8. Put every preview-safe value in the application's **base** environment variables, not in Coolify's
53
83
separate preview-variable scope. See below.
54
84
55
85
## Why the safe values live in Coolify's base scope
0 commit comments