80 lines
5.4 KiB
Markdown
80 lines
5.4 KiB
Markdown
# Security Architecture
|
|
|
|
## The Server Boundary Rule
|
|
|
|
**No server-side credential may cross the server boundary to the client. Ever.**
|
|
|
|
This is a hard line, not a guideline. The only credentials that cross the boundary between client and server are the client's own identity credentials (MFA tokens, login passwords, OIDC tokens, etc.) — and these flow from client to server, never the reverse.
|
|
|
|
### What this means in practice
|
|
|
|
- **API tokens stay server-side.** If a browser-based application needs to call a third-party API (Gitea, S3, database, etc.), it calls a backend proxy that holds the token. The token never appears in JavaScript, localStorage, cookies, or any client-accessible storage.
|
|
- **Service account credentials stay server-side.** Database passwords, S3 access keys, webhook secrets, SMTP credentials — these are mounted into server-side containers via Kubernetes Secrets or environment variables and never exposed to clients.
|
|
- **OAuth tokens for third-party services stay server-side.** If a user authenticates with Service A and the application needs to call Service B on their behalf, the application's backend holds the Service B credentials. The client only ever sees its own session token with the application.
|
|
- **Per-user tokens mapped server-side.** When individual users need distinct third-party access (e.g., per-user Gitea tokens for audit trails), the mapping from user identity to their token lives on the server. The client authenticates with its own identity (e.g., via Authelia MFA), and the server looks up the appropriate third-party token.
|
|
|
|
### The identity exception
|
|
|
|
The only credentials that legitimately cross from client to server:
|
|
|
|
- **Username + password** — the user's own login credentials
|
|
- **MFA tokens** — TOTP codes, WebAuthn assertions, security key responses
|
|
- **OIDC/OAuth tokens** — tokens that represent the user's identity with the application itself (not with third-party services)
|
|
- **Session cookies/JWTs** — issued by the server to represent an authenticated session
|
|
|
|
These all share the property: they are the user's own identity, flowing from client to server for authentication purposes.
|
|
|
|
### Architecture pattern: proxy with identity mapping
|
|
|
|
When a client-side application (SPA, CMS, admin UI) needs to interact with a backend service that requires credentials:
|
|
|
|
```
|
|
Client ──→ Auth Layer (MFA) ──→ API Proxy ──→ Backend Service
|
|
│
|
|
├── Reads user identity from auth headers
|
|
├── Looks up user's backend credential
|
|
├── Forwards request with backend credential
|
|
└── Returns response (without credential)
|
|
```
|
|
|
|
The proxy:
|
|
1. Sits behind the authentication layer (Authelia, OAuth2 Proxy, etc.)
|
|
2. Reads the authenticated user's identity from trusted headers (e.g., `Remote-User`)
|
|
3. Maps the identity to the appropriate backend credential
|
|
4. Makes the backend API call with the credential
|
|
5. Returns the response — never the credential
|
|
|
|
### Security layers (defense in depth)
|
|
|
|
A well-designed proxy architecture has multiple independent security layers:
|
|
|
|
1. **Authentication** — User must prove their identity (MFA, OIDC)
|
|
2. **Session validation** — Proxy validates the session is current and legitimate
|
|
3. **Authorization** — Proxy checks the user has access to the requested resource
|
|
4. **Backend permissions** — The backend service enforces its own access controls
|
|
5. **Branch/scope protection** — Fine-grained controls prevent privilege escalation (e.g., branch protection rules)
|
|
|
|
Each layer is independent — compromising one doesn't bypass the others.
|
|
|
|
### Anti-patterns
|
|
|
|
- **Passing API tokens to the browser via OAuth.** Even with PKCE, the token ends up in client-accessible storage. Use a backend proxy instead.
|
|
- **Shared service account tokens.** One token for all users means no audit trail and no granular revocation. Map per-user tokens server-side.
|
|
- **Embedding credentials in client-side config.** API keys in `config.js`, `.env` files served statically, or hardcoded in HTML — all violate the boundary rule.
|
|
- **Forwarding backend tokens via API responses.** Even "temporarily" returning a token in a response body breaks the rule. The client should never see it.
|
|
- **Using the same token for client auth and backend calls.** The user's session token with your application is distinct from any token your application uses to call backend services.
|
|
|
|
### When credentials must be client-side
|
|
|
|
Some scenarios genuinely require client-side credentials (e.g., direct S3 uploads for large files, WebRTC signaling). In these cases:
|
|
- Use **presigned URLs** or **temporary credentials** with the narrowest possible scope and shortest possible lifetime
|
|
- The presigning/credential-issuance happens server-side
|
|
- The temporary credential is scoped to exactly one operation (e.g., upload one file to one path)
|
|
- Log the issuance server-side for audit
|
|
|
|
### Real-world example: CMS editing
|
|
|
|
**Wrong:** CMS authenticates directly with Gitea via OAuth popup. Gitea token lands in browser localStorage. CMS makes API calls directly to Gitea with the token.
|
|
|
|
**Right:** CMS sits behind Authelia (MFA). A proxy service intercepts the OAuth flow, issues a proxy session token (containing only the user's identity), and forwards all API calls to Gitea using a per-user server-side Gitea token. The Gitea token never leaves the server.
|