PDS Admin Portal#
Overview#
Bluesky'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.
The 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.
Prerequisites#
- A PDS instance running behind pds-gatekeeper
- HTTPS with a valid TLS certificate (required for ATProto OAuth flows)
- SMTP configured on the PDS for email functionality (used by the PDS itself, not strictly by the admin portal)
Quick Start#
1. Create an RBAC configuration file#
Copy the example configuration as a starting point:
cp examples/admin_rbac.yaml /path/to/your/admin_rbac.yaml
2. Find your team members' DIDs#
Use goat to resolve a handle to its DID:
goat resolve alice.example.com
The DID is the id field in the output (e.g. did:plc:abcdef1234567890).
3. Set environment variables#
# Required
GATEKEEPER_ADMIN_RBAC_CONFIG=/path/to/your/admin_rbac.yaml
PDS_ADMIN_PASSWORD=your-pds-admin-password
# Optional
GATEKEEPER_ADMIN_COOKIE_SECRET=<64-character-hex-string>
GATEKEEPER_ADMIN_SESSION_TTL_HOURS=24
4. Restart pds-gatekeeper#
# If running with systemd:
sudo systemctl restart pds-gatekeeper
# If running with Docker:
docker restart pds-gatekeeper
5. Navigate to the admin portal#
Open your browser and go to:
https://your-pds.example.com/admin/login
RBAC Configuration#
The RBAC configuration is a YAML file with two top-level sections: roles and members.
- Roles define named sets of endpoint patterns that grant access to specific admin operations.
- Members map an ATProto DID to one or more roles.
A member's effective permissions are the union of all endpoints from all of their assigned roles.
Endpoint patterns support wildcard matching: com.atproto.admin.* matches all endpoints under the com.atproto.admin namespace.
Example:
roles:
pds-admin:
endpoints:
- "com.atproto.admin.*"
- "com.atproto.server.createInviteCode"
- "com.atproto.server.createAccount"
moderator:
endpoints:
- "com.atproto.admin.getAccountInfo"
- "com.atproto.admin.getAccountInfos"
- "com.atproto.admin.getSubjectStatus"
- "com.atproto.admin.updateSubjectStatus"
- "com.atproto.admin.sendEmail"
- "com.atproto.admin.getInviteCodes"
invite-manager:
endpoints:
- "com.atproto.server.createInviteCode"
- "com.atproto.admin.getInviteCodes"
- "com.atproto.admin.disableInviteCodes"
- "com.atproto.admin.enableAccountInvites"
- "com.atproto.admin.disableAccountInvites"
members:
- did: "did:plc:abcdef1234567890"
roles:
- pds-admin
- did: "did:plc:bbbbbbbbbbbbbbbb"
roles:
- moderator
- invite-manager
Available Roles (Reference)#
Suggested role templates#
You can make your own roles and teams
| Role | Description | Endpoints |
|---|---|---|
pds-admin |
Full administrative access | com.atproto.admin.*, createInviteCode, createAccount |
moderator |
View accounts, manage takedowns, send email, view invite codes | getAccountInfo, getAccountInfos, getSubjectStatus, updateSubjectStatus, sendEmail, getInviteCodes |
invite-manager |
Manage invite codes and per-account invite permissions | createInviteCode, getInviteCodes, disableInviteCodes, enableAccountInvites, disableAccountInvites |
All admin XRPC endpoints#
| Endpoint | Description |
|---|---|
com.atproto.admin.getAccountInfo |
View single account details |
com.atproto.admin.getAccountInfos |
View multiple accounts |
com.atproto.admin.getSubjectStatus |
Get takedown status |
com.atproto.admin.updateSubjectStatus |
Apply or remove takedowns |
com.atproto.admin.deleteAccount |
Permanently delete an account |
com.atproto.admin.updateAccountPassword |
Reset account password |
com.atproto.admin.enableAccountInvites |
Enable invites for an account |
com.atproto.admin.disableAccountInvites |
Disable invites for an account |
com.atproto.admin.getInviteCodes |
List invite codes |
com.atproto.admin.disableInviteCodes |
Disable specific invite codes |
com.atproto.admin.sendEmail |
Send email to an account |
com.atproto.server.createInviteCode |
Create a new invite code |
com.atproto.server.createAccount |
Create a new account |
How It Works#
1. OAuth Login#
The 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.
2. Session Creation#
Gatekeeper 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.
3. Request Flow#
When the user performs an admin action, gatekeeper:
- Validates the session cookie signature and expiration
- Looks up the user's DID in the RBAC configuration
- Checks whether the user's roles grant access to the target XRPC endpoint
- If authorized, proxies the request to the PDS with
Authorization: BasicusingPDS_ADMIN_PASSWORD - Returns the PDS response to the user
4. UI Rendering#
The 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.
Environment Variables#
| Variable | Required | Default | Description |
|---|---|---|---|
GATEKEEPER_ADMIN_RBAC_CONFIG |
Yes | — | Path to the RBAC YAML configuration file |
PDS_ADMIN_PASSWORD |
Yes | — | PDS admin password used for proxied requests |
GATEKEEPER_ADMIN_COOKIE_SECRET |
No | Derived from GATEKEEPER_JWE_KEY |
32-byte hex key for signing session cookies |
GATEKEEPER_ADMIN_SESSION_TTL_HOURS |
No | 24 |
Admin session lifetime in hours |
Security Considerations#
- Password isolation:
PDS_ADMIN_PASSWORDis never sent to or accessible from browsers. It is only used server-side when proxying authorized requests to the PDS. - OAuth security: The OAuth flow uses DPoP binding and PKCE to prevent token interception and replay attacks.
- Cookie protections: Session cookies are signed (tamper-proof) and set with
HttpOnly,Secure, andSameSite=Laxattributes. - Server-side enforcement: RBAC is enforced in route handlers, not just in template rendering. Manipulating the UI cannot bypass access controls.
- Session lifecycle: Sessions expire after a configurable TTL. Expired sessions are cleaned up automatically.
- Opt-in activation: The admin portal is completely opt-in. If
GATEKEEPER_ADMIN_RBAC_CONFIGis not set, no admin routes are mounted and the portal is entirely inactive.
Troubleshooting#
OAuth callback failures Ensure 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.
"Access Denied" after login
Verify that the DID in your RBAC configuration exactly matches the DID of the authenticating identity. Use goat resolve {handle} to confirm the correct DID.
Session expired
Sessions expire after the configured TTL (default 24 hours). Either increase GATEKEEPER_ADMIN_SESSION_TTL_HOURS or log in again.
403 on admin action
The 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.
Admin portal not appearing
Confirm 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.