Microservice to bring 2FA to self hosted PDSes
at feature/admin-rbac 201 lines 8.7 kB view raw view rendered
1# PDS Admin Portal 2 3## Overview 4 5Bluesky's PDS admin API relies on `PDS_ADMIN_PASSWORD` — a single shared secret that grants unrestricted access to every administrative endpoint. This is workable for solo operators but becomes a liability when multiple team members need admin access. There is no way to limit what any individual can do, no audit trail of who performed an action, and credential rotation affects everyone simultaneously. 6 7The pds-gatekeeper admin portal solves this by introducing role-based access control (RBAC). Team members authenticate with ATProto OAuth using their own identity, and gatekeeper enforces per-user permissions based on a YAML configuration file. Authorized requests are proxied to the PDS using the admin password on behalf of the authenticated user — the password itself is never exposed to browsers or end users. 8 9## Prerequisites 10 11- A PDS instance running behind pds-gatekeeper 12- HTTPS with a valid TLS certificate (required for ATProto OAuth flows) 13- SMTP configured on the PDS for email functionality (used by the PDS itself, not strictly by the admin portal) 14 15## Quick Start 16 17### 1. Create an RBAC configuration file 18 19Copy the example configuration as a starting point: 20 21```sh 22cp examples/admin_rbac.yaml /path/to/your/admin_rbac.yaml 23``` 24 25### 2. Find your team members' DIDs 26 27Use [`goat`](https://github.com/bluesky-social/indigo/tree/main/cmd/goat) to resolve a handle to its DID: 28 29```sh 30goat resolve alice.example.com 31``` 32 33The DID is the `id` field in the output (e.g. `did:plc:abcdef1234567890`). 34 35### 3. Set environment variables 36 37```sh 38# Required 39GATEKEEPER_ADMIN_RBAC_CONFIG=/path/to/your/admin_rbac.yaml 40PDS_ADMIN_PASSWORD=your-pds-admin-password 41 42# Optional 43GATEKEEPER_ADMIN_COOKIE_SECRET=<64-character-hex-string> 44GATEKEEPER_ADMIN_SESSION_TTL_HOURS=24 45``` 46 47### 4. Restart pds-gatekeeper 48 49```sh 50# If running with systemd: 51sudo systemctl restart pds-gatekeeper 52 53# If running with Docker: 54docker restart pds-gatekeeper 55``` 56 57### 5. Navigate to the admin portal 58 59Open your browser and go to: 60 61``` 62https://your-pds.example.com/admin/login 63``` 64 65## RBAC Configuration 66 67The RBAC configuration is a YAML file with two top-level sections: `roles` and `members`. 68 69- **Roles** define named sets of endpoint patterns that grant access to specific admin operations. 70- **Members** map an ATProto DID to one or more roles. 71 72A member's effective permissions are the **union** of all endpoints from all of their assigned roles. 73 74Endpoint patterns support wildcard matching: `com.atproto.admin.*` matches all endpoints under the `com.atproto.admin` namespace. 75 76Example: 77 78```yaml 79roles: 80 pds-admin: 81 endpoints: 82 - "com.atproto.admin.*" 83 - "com.atproto.server.createInviteCode" 84 - "com.atproto.server.createAccount" 85 86 moderator: 87 endpoints: 88 - "com.atproto.admin.getAccountInfo" 89 - "com.atproto.admin.getAccountInfos" 90 - "com.atproto.admin.getSubjectStatus" 91 - "com.atproto.admin.updateSubjectStatus" 92 - "com.atproto.admin.sendEmail" 93 - "com.atproto.admin.getInviteCodes" 94 95 invite-manager: 96 endpoints: 97 - "com.atproto.server.createInviteCode" 98 - "com.atproto.admin.getInviteCodes" 99 - "com.atproto.admin.disableInviteCodes" 100 - "com.atproto.admin.enableAccountInvites" 101 - "com.atproto.admin.disableAccountInvites" 102 103members: 104 - did: "did:plc:abcdef1234567890" 105 roles: 106 - pds-admin 107 108 - did: "did:plc:bbbbbbbbbbbbbbbb" 109 roles: 110 - moderator 111 - invite-manager 112``` 113 114## Available Roles (Reference) 115 116### Suggested role templates 117 118You can make your own roles and teams 119 120| Role | Description | Endpoints | 121|---|---|---| 122| `pds-admin` | Full administrative access | `com.atproto.admin.*`, `createInviteCode`, `createAccount` | 123| `moderator` | View accounts, manage takedowns, send email, view invite codes | `getAccountInfo`, `getAccountInfos`, `getSubjectStatus`, `updateSubjectStatus`, `sendEmail`, `getInviteCodes` | 124| `invite-manager` | Manage invite codes and per-account invite permissions | `createInviteCode`, `getInviteCodes`, `disableInviteCodes`, `enableAccountInvites`, `disableAccountInvites` | 125 126### All admin XRPC endpoints 127 128| Endpoint | Description | 129|---|---| 130| `com.atproto.admin.getAccountInfo` | View single account details | 131| `com.atproto.admin.getAccountInfos` | View multiple accounts | 132| `com.atproto.admin.getSubjectStatus` | Get takedown status | 133| `com.atproto.admin.updateSubjectStatus` | Apply or remove takedowns | 134| `com.atproto.admin.deleteAccount` | Permanently delete an account | 135| `com.atproto.admin.updateAccountPassword` | Reset account password | 136| `com.atproto.admin.enableAccountInvites` | Enable invites for an account | 137| `com.atproto.admin.disableAccountInvites` | Disable invites for an account | 138| `com.atproto.admin.getInviteCodes` | List invite codes | 139| `com.atproto.admin.disableInviteCodes` | Disable specific invite codes | 140| `com.atproto.admin.sendEmail` | Send email to an account | 141| `com.atproto.server.createInviteCode` | Create a new invite code | 142| `com.atproto.server.createAccount` | Create a new account | 143 144## How It Works 145 146### 1. OAuth Login 147 148The user navigates to `/admin/login` and enters their ATProto handle. Gatekeeper initiates an OAuth authorization flow, redirecting the user to their identity's authorization server. The user authenticates there and is redirected back to gatekeeper with an authorization code. 149 150### 2. Session Creation 151 152Gatekeeper exchanges the authorization code for tokens, extracts the user's DID from the OAuth session, and checks it against the RBAC configuration. If the DID is found in the members list, a signed session cookie is created and set in the browser. 153 154### 3. Request Flow 155 156When the user performs an admin action, gatekeeper: 157 1581. Validates the session cookie signature and expiration 1592. Looks up the user's DID in the RBAC configuration 1603. Checks whether the user's roles grant access to the target XRPC endpoint 1614. If authorized, proxies the request to the PDS with `Authorization: Basic` using `PDS_ADMIN_PASSWORD` 1625. Returns the PDS response to the user 163 164### 4. UI Rendering 165 166The admin portal uses server-rendered pages that show or hide actions based on the authenticated user's permissions. However, RBAC is always enforced server-side in route handlers regardless of what the UI displays — hiding a button in the template is a convenience, not a security boundary. 167 168## Environment Variables 169 170| Variable | Required | Default | Description | 171|---|---|---|---| 172| `GATEKEEPER_ADMIN_RBAC_CONFIG` | Yes | — | Path to the RBAC YAML configuration file | 173| `PDS_ADMIN_PASSWORD` | Yes | — | PDS admin password used for proxied requests | 174| `GATEKEEPER_ADMIN_COOKIE_SECRET` | No | Derived from `GATEKEEPER_JWE_KEY` | 32-byte hex key for signing session cookies | 175| `GATEKEEPER_ADMIN_SESSION_TTL_HOURS` | No | `24` | Admin session lifetime in hours | 176 177## Security Considerations 178 179- **Password isolation**: `PDS_ADMIN_PASSWORD` is never sent to or accessible from browsers. It is only used server-side when proxying authorized requests to the PDS. 180- **OAuth security**: The OAuth flow uses DPoP binding and PKCE to prevent token interception and replay attacks. 181- **Cookie protections**: Session cookies are signed (tamper-proof) and set with `HttpOnly`, `Secure`, and `SameSite=Lax` attributes. 182- **Server-side enforcement**: RBAC is enforced in route handlers, not just in template rendering. Manipulating the UI cannot bypass access controls. 183- **Session lifecycle**: Sessions expire after a configurable TTL. Expired sessions are cleaned up automatically. 184- **Opt-in activation**: The admin portal is completely opt-in. If `GATEKEEPER_ADMIN_RBAC_CONFIG` is not set, no admin routes are mounted and the portal is entirely inactive. 185 186## Troubleshooting 187 188**OAuth callback failures** 189Ensure HTTPS is properly configured with a valid certificate, DNS resolves correctly for your PDS hostname, and the hostname the user accesses matches the PDS configuration. 190 191**"Access Denied" after login** 192Verify that the DID in your RBAC configuration exactly matches the DID of the authenticating identity. Use `goat resolve {handle}` to confirm the correct DID. 193 194**Session expired** 195Sessions expire after the configured TTL (default 24 hours). Either increase `GATEKEEPER_ADMIN_SESSION_TTL_HOURS` or log in again. 196 197**403 on admin action** 198The authenticated user's roles do not include the endpoint being accessed. Check the `members` and `roles` sections of your RBAC config to ensure the required endpoint pattern is granted. 199 200**Admin portal not appearing** 201Confirm that `GATEKEEPER_ADMIN_RBAC_CONFIG` is set in the environment, the file path is correct, and the file exists and is readable by the gatekeeper process.