45 KiB
Architecture Deep-Dive
This document provides a detailed technical explanation of how the Organization Stack components interact, including protocol details, authentication flows, and design decisions.
Table of Contents
- System Architecture
- Authentication Protocols
- Component Details
- Security Model
- Network Architecture
- Data Flow Examples
- Design Decisions
System Architecture
┌────────────────────────────────────────────────────────────────────────────────────────┐
│ Internet / Users │
│ │
│ https://git.example.com https://wiki.example.com https://register.example.com │
└──────────────────────────────────┬─────────────────────────────────────────────────────┘
│
│ HTTPS (TLS 1.2+)
▼
┌─────────────────────┐
│ Caddy │
│ Reverse Proxy │
│ - TLS Termination │
│ - Forward-Auth │
│ - HTTP Routing │
└──────────┬──────────┘
│
┌─────────────────────────┼──────────────────────────┬──────────────────────┐
│ │ │ │
│ OIDC │ Forward-Auth │ Forward-Auth │ Public + Forward-Auth
▼ ▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Gitea │ │ JSPWiki │ │ lldap (Web) │ │ Registration │
│ │ │ │ │ │ │ │
│ Port: 3000 │ │ Port: 8080 │ │ Port: 17170 │ │ Port: 5000 │
│ │ │ │ │ │ │ │
│ Auth: OIDC │ │ Auth: Remote- │ │ Auth: Remote- │ │ / - Public │
│ (OAuth2) │ │ User Header │ │ User Header │ │ /admin - Auth │
└────────┬────────┘ └────────┬────────┘ └─────────────────┘ └────────┬────────┘
│ │ │
│ OIDC Protocol │ LDAP Query LDAP API│
│ │ (User Sync) (Create User)
▼ ▼ │
┌────────────────────────────────────────────┐ │
│ Authelia │ │
│ │ │
│ - OIDC Provider (/.well-known/...) │ │
│ - Forward-Auth Endpoint (/api/authz/...) │ │
│ - Session Management │ │
│ - 2FA/TOTP Validation │ │
│ - Access Control Policies │ │
│ │ │
│ Port: 9091 │ │
└──────────────────┬─────────────────────────┘ │
│ │
│ LDAP Protocol │
│ (Credential Validation) │
▼ ▼
┌──────────────────────┐ ◄─────────────────────────────────────────────────┘
│ lldap │
│ │
│ User Database │
│ - Users │
│ - Groups │
│ - Credentials │
│ │
│ LDAP: 3890 │
│ HTTP: 17170 │
└──────────────────────┘
Authentication Protocols
LDAP Integration
What is LDAP?
LDAP (Lightweight Directory Access Protocol) is a protocol for accessing and maintaining distributed directory information services. Think of it as a specialized database optimized for read-heavy operations on hierarchical data structures.
How we use it:
lldap Storage Structure:
dc=example,dc=com (Base DN)
├── ou=people (Users)
│ ├── uid=alice (User Entry)
│ │ ├── cn: Alice Smith
│ │ ├── mail: alice@example.com
│ │ ├── userPassword: {SSHA}...
│ │ └── objectClass: person
│ └── uid=bob
│ ├── cn: Bob Jones
│ └── ...
└── ou=groups (Groups)
└── cn=lldap_admin (Admin Group)
├── member: uid=alice,ou=people,dc=example,dc=com
└── objectClass: groupOfUniqueNames
Authentication Flow:
-
Authelia connects to lldap using bind credentials:
BIND uid=admin,ou=people,dc=example,dc=com PASSWORD: <from secrets/lldap/LDAP_USER_PASS> -
User attempts login with username "alice"
-
Authelia performs LDAP search:
SEARCH base_dn: ou=people,dc=example,dc=com FILTER: (&(uid=alice)(objectClass=person)) -
Authelia validates password by attempting bind as user:
BIND uid=alice,ou=people,dc=example,dc=com PASSWORD: <user's password> -
If successful, Authelia queries groups:
SEARCH base_dn: ou=groups,dc=example,dc=com FILTER: (member=uid=alice,ou=people,dc=example,dc=com)
References:
OIDC Flow
What is OpenID Connect?
OIDC is an identity layer built on top of OAuth 2.0. It allows clients to verify user identity and obtain basic profile information in an interoperable REST-like manner.
Components:
- Provider (OP): Authelia - issues tokens, manages authentication
- Relying Party (RP): Gitea - trusts tokens from provider
Authorization Code Flow (Detailed):
User Gitea Authelia lldap
│ │ │ │
│ 1. GET /login │ │ │
├──────────────────────►│ │ │
│ │ │ │
│ │ 2. Redirect to Authelia │ │
│ │ /authorize? │ │
│ │ client_id=gitea │ │
│ │ &redirect_uri=... │ │
│ │ &scope=openid... │ │
│◄──────────────────────┤ │ │
│ │ │ │
│ 3. GET /authorize │ │ │
├───────────────────────┴─────────────────────────► │
│ │ │
│ 4. Login form (if not authenticated) │ │
│◄────────────────────────────────────────────────┤ │
│ │ │
│ 5. POST credentials │ │
├─────────────────────────────────────────────────► │
│ │ │
│ │ 6. Validate password │
│ ├─────────────────────►│
│ │ LDAP BIND │
│ │◄─────────────────────┤
│ │ SUCCESS │
│ │ │
│ 7. TOTP prompt (if 2FA enabled) │ │
│◄────────────────────────────────────────────────┤ │
│ │ │
│ 8. POST TOTP code │ │
├─────────────────────────────────────────────────► │
│ │ │
│ 9. Redirect to Gitea with code │ │
│ /callback?code=abc123 │ │
│◄────────────────────────────────────────────────┤ │
│ │ │ │
│ 10. GET /callback? │ │ │
│ code=abc123 │ │ │
├──────────────────────►│ │ │
│ │ │ │
│ │ 11. POST /token │ │
│ │ code=abc123 │ │
│ │ client_secret=... │ │
│ ├─────────────────────────► │
│ │ │ │
│ │ 12. Access token + │ │
│ │ ID token │ │
│ │◄────────────────────────┤ │
│ │ │ │
│ │ 13. GET /userinfo │ │
│ ├─────────────────────────► │
│ │ │ │
│ │ 14. User profile │ │
│ │◄────────────────────────┤ │
│ │ │ │
│ 15. Logged in │ │ │
│◄──────────────────────┤ │ │
Key Endpoints:
Authelia provides these OIDC endpoints:
/.well-known/openid-configuration- Discovery document/api/oidc/authorization- Authorization endpoint/api/oidc/token- Token endpoint/api/oidc/userinfo- User info endpoint/api/oidc/jwks- Public keys for token verification
Token Format (JWT):
{
"header": {
"alg": "RS256",
"kid": "key-id"
},
"payload": {
"iss": "https://auth.example.com",
"sub": "alice",
"aud": "gitea",
"exp": 1234567890,
"iat": 1234564290,
"email": "alice@example.com",
"preferred_username": "alice",
"groups": ["lldap_admin"]
},
"signature": "..."
}
References:
Forward-Auth Pattern
What is Forward-Auth?
Forward-Auth is a proxy-level authentication pattern where a reverse proxy delegates authentication decisions to an external service before forwarding requests to backends.
How it works:
User Caddy Authelia JSPWiki
│ │ │ │
│ 1. GET /wiki/Main │ │ │
├────────────────────►│ │ │
│ │ │ │
│ │ 2. Sub-request to │ │
│ │ /api/authz/ │ │
│ │ forward-auth │ │
│ ├────────────────────────► │
│ │ (with all cookies) │ │
│ │ │ │
│ │ 3a. If NOT authenticated │
│ │ 401 + redirect URL │ │
│ │◄───────────────────────┤ │
│ │ │ │
│ 4. Redirect to │ │ │
│ Authelia login │ │ │
│◄────────────────────┤ │ │
│ │ │ │
│ [User logs in with │ │ │
│ credentials + 2FA] │ │ │
│ │ │ │
│ 5. Redirect back │ │ │
│ to /wiki/Main │ │ │
├────────────────────►│ │ │
│ │ │ │
│ │ 6. Sub-request to │ │
│ │ /api/authz/ │ │
│ │ forward-auth │ │
│ ├────────────────────────► │
│ │ (with session │ │
│ │ cookie) │ │
│ │ │ │
│ │ 7. 200 OK + │ │
│ │ Remote-User: alice │ │
│ │ Remote-Groups: ... │ │
│ │◄───────────────────────┤ │
│ │ │ │
│ │ 8. Forward to JSPWiki │ │
│ │ with Remote-User │ │
│ │ header │ │
│ ├──────────────────────────────────────────────►│
│ │ │ │
│ │ 9. Response │ │
│ │◄──────────────────────────────────────────────┤
│ │ │ │
│ 10. Page delivered │ │ │
│◄────────────────────┤ │ │
Caddyfile Configuration:
# Reusable forward-auth snippet
(auth) {
forward_auth authelia:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
}
# Wiki with authentication
wiki.example.com {
import auth
reverse_proxy jspwiki:8080
}
Headers Passed:
When authentication succeeds, Authelia returns these headers:
Remote-User: Username (e.g., "alice")Remote-Groups: Comma-separated groups (e.g., "lldap_admin,developers")Remote-Email: User's emailRemote-Name: Display name
JSPWiki Container Authentication:
JSPWiki is configured to trust the Remote-User header via our custom RemoteUserFilter:
// RemoteUserFilter.java
public class RemoteUserFilter implements Filter {
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String remoteUser = httpRequest.getHeader("Remote-User");
if (remoteUser != null && !remoteUser.isEmpty()) {
// Wrap request to return Remote-User as getRemoteUser()
HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(httpRequest) {
@Override
public String getRemoteUser() {
return remoteUser;
}
@Override
public Principal getUserPrincipal() {
return new Principal() {
public String getName() { return remoteUser; }
};
}
};
chain.doFilter(wrapper, response);
} else {
chain.doFilter(request, response);
}
}
}
Security Considerations:
⚠️ Critical: The backend MUST NOT be accessible except through the proxy. If an attacker can reach JSPWiki directly, they can forge the Remote-User header.
Our setup ensures this by:
- All services on internal Docker network
- Only Caddy exposes external ports
- JSPWiki port 8080 not exposed to host
References:
Component Details
lldap - LDAP Directory
Purpose: Central user and group database
Why LDAP?
- Industry standard protocol
- Supported by virtually all enterprise software
- Hierarchical data model perfect for organizational structure
- Read-optimized for authentication use cases
Why lldap specifically?
- Lightweight alternative to heavyweight solutions (OpenLDAP, Active Directory)
- Simple web UI for user management
- SQLite backend (no PostgreSQL/MySQL dependency)
- Built in Rust for memory safety
Schema:
objectClass: person
├── uid (username)
├── cn (common name / display name)
├── mail (email address)
├── userPassword (hashed password)
└── ... (standard LDAP attributes)
objectClass: groupOfUniqueNames
├── cn (group name)
└── member (DN of member users)
Ports:
- 3890: LDAP protocol
- 17170: Web admin interface
Authelia - SSO Server
Purpose: Central authentication authority, session management, 2FA enforcement
Why Authelia?
- Supports multiple auth methods (LDAP, file, etc.)
- Built-in TOTP/2FA support
- Both OIDC provider AND forward-auth endpoint
- Flexible access control policies
- Session management with Redis or SQLite
Key Features:
- Multi-factor Authentication: TOTP (RFC 6238), WebAuthn planned
- Single Sign-On: One login session across all services
- Access Control: Domain-based policies (bypass, one_factor, two_factor)
- Session Management: Configurable lifespans, remember-me
- Rate Limiting: Brute-force protection
Configuration Structure:
authentication_backend: # How to validate credentials
ldap: # Use LDAP (lldap)
address: ldap://lldap:3890
base_dn: dc=example,dc=com
access_control: # Who can access what
default_policy: deny # Deny by default
rules: # Explicit allow rules
- domain: wiki.example.com
policy: two_factor # Require 2FA
identity_providers: # What protocols to provide
oidc: # OIDC provider config
clients:
- client_id: gitea
authorization_policy: two_factor
Storage:
- SQLite database in
/data/db.sqlite3 - Contains: sessions, 2FA registrations, access logs
Gitea - Git Hosting
Purpose: Self-hosted Git repository platform
Authentication Method: OIDC (OAuth 2.0 + OpenID Connect)
Why OIDC for Gitea?
- Gitea has native OIDC support
- Allows full SSO experience (no double-login)
- Automatic user provisioning from OIDC claims
- Group synchronization via OIDC groups claim
Configuration:
[auth]
ENABLE_OAUTH2 = true
[openid]
ENABLE_OPENID_SIGNIN = true
ENABLE_OPENID_SIGNUP = true
Configured via web UI to use Authelia as OIDC provider.
⚠️ Important: The authentication source name in Gitea must be exactly authelia (lowercase). Gitea constructs the redirect URI using this name: /user/oauth2/{name}/callback. Since OAuth2 redirect URIs are case-sensitive, using "Authelia" (capital A) will cause authentication to fail with an invalid_request error.
JSPWiki - Wiki Platform
Purpose: Collaborative wiki and knowledge base
Authentication Method: Container authentication via forward-auth
Why not OIDC for JSPWiki? JSPWiki doesn't natively support OIDC, but it does support "container authentication" (trusting the servlet container's authentication).
Our Solution:
- Caddy enforces authentication via forward-auth
- Caddy passes
Remote-Userheader - Our custom
RemoteUserFilterservlet filter wraps the request - JSPWiki sees an authenticated user via standard servlet APIs (
request.getRemoteUser())
LDAP Synchronization:
JSPWiki needs users in its own XML databases for permissions. Our ldap-sync.sh script:
- Queries lldap via
ldapsearch - Generates
userdatabase.xmlwith all LDAP users - Maps
lldap_admingroup to JSPWiki'sAdmingroup - Runs on container startup
Files:
/var/jspwiki/pages/- Wiki pages (markdown)/var/jspwiki/etc/userdatabase.xml- User database (synced from LDAP)/var/jspwiki/etc/groupdatabase.xml- Group mappings
Registration Service - User Self-Provisioning
Purpose: Allow users to request accounts without admin intervention in lldap
Architecture: FastAPI + SQLite + pure LDAP operations
Features:
- Public registration form
- Admin approval workflow
- Audit trail of all decisions
- Automatic user creation in lldap
- Secure random password generation
Authentication Methods:
- Public Route (
/) - Registration form (no auth required) - Protected Route (
/admin) - Admin dashboard (forward-auth via Authelia)
Workflow:
┌─────────────┐
│ User │
│ (Public) │
└──────┬──────┘
│
│ 1. Fill registration form
│ (username, email, name, reason)
▼
┌────────────────────┐
│ Registration │
│ Service │
│ (FastAPI) │
└─────┬──────────────┘
│
│ 2. Store in SQLite
│ status='pending'
│
│ 3. Notify admin (email)
│
▼
┌────────────────────┐
│ Admin │
│ (Authenticated) │
└─────┬──────────────┘
│
│ 4. Review at /admin
│ (protected by Authelia)
│
│ 5. Approve or Reject
│
▼
┌────────────────────┐
│ Registration │
│ Service │
│ (LDAP Client) │
└─────┬──────────────┘
│
│ 6. If approved:
│ - Generate random password
│ - Create user via LDAP
│
▼
┌────────────────────┐
│ lldap │
│ (LDAP Server) │
└─────┬──────────────┘
│
│ 7. User created
│
▼
┌────────────────────┐
│ User │
│ (Email) │
└────────────────────┘
8. Receive credentials
via email
Database Schema:
-- Pending registration requests
CREATE TABLE registration_requests (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
reason TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address TEXT,
user_agent TEXT
);
-- Historical audit log
CREATE TABLE audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL,
email TEXT NOT NULL,
first_name TEXT NOT NULL,
last_name TEXT NOT NULL,
reason TEXT,
action TEXT NOT NULL,
performed_by TEXT,
rejection_reason TEXT,
ip_address TEXT,
user_agent TEXT,
created_at TIMESTAMP,
reviewed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
Workflow: Request submitted → Pending queue → Approve/Reject → Audit log
lldap LDAP Integration:
The service creates users in lldap via pure LDAP operations using the admin credentials from /secrets-lldap/LDAP_USER_PASS:
# 1. Create user entry with ldapadd
LDIF = '''
dn: uid=newuser,ou=people,dc=example,dc=com
objectClass: person
objectClass: inetOrgPerson
uid: newuser
cn: New User
sn: User
givenName: New
mail: user@example.com
'''
subprocess.run([
'ldapadd',
'-H', 'ldap://lldap:3890',
'-D', 'uid=admin,ou=people,dc=example,dc=com',
'-w', '<admin_password>',
'-x'
], input=LDIF)
# 2. Set password with ldappasswd
subprocess.run([
'ldappasswd',
'-H', 'ldap://lldap:3890',
'-D', 'uid=admin,ou=people,dc=example,dc=com',
'-w', '<admin_password>',
'-s', '<random_generated_password>',
'uid=newuser,ou=people,dc=example,dc=com'
])
Benefits of Pure LDAP Approach:
- No HTTP client dependency (removed httpx)
- Standard LDAP tools (
ldapadd,ldappasswd) - Simpler code and fewer external dependencies
- Works with any LDAP-compliant directory server
Configuration (.env):
REGISTRATION_SUBDOMAIN=registerREGISTRATION_ADMIN_EMAIL- For admin notificationsSMTP_ENABLED=false- Enable email notifications
Security:
- LDAP Injection Prevention:
- RFC 4514 compliant DN escaping
- Strict username validation (alphanumeric + underscore only)
- Email format validation
- Defense-in-depth: validation at form submission AND before LDAP operations
- Input Validation:
- Username: 2-64 chars, must start with letter, lowercase only
- Email: basic format validation
- Names: max 100 characters
- Authentication: Admin dashboard protected by Authelia
- Secrets: File-based, read-only mounts
- Password Generation: Cryptographically secure (20 chars)
- Audit Trail: Full logging of all approval/rejection decisions
Storage:
- SQLite database:
/data/registrations.db - Backed up with Docker volume
registration_data
Port: 5000 (internal, not exposed - accessed via Caddy)
User Management
Single Source of Truth: lldap
The registration service handles the approval workflow only. Once approved, users are created in lldap and managed there exclusively.
Registration Service → Handles approval workflow
Creates users in lldap when approved
lldap → Manages all active users
User attributes, groups, credentials
Admin Workflow:
- Review pending requests at
/admin - Approve → User created in lldap automatically
- Reject → Logged to audit trail
- Manage active users in lldap web interface
API Endpoints:
GET /admin- View pending requests and audit logPOST /admin/approve/{id}- Create user in lldap, log approvalPOST /admin/reject/{id}- Log rejection with reason
Caddy - Reverse Proxy
Purpose: TLS termination, routing, authentication enforcement
Why Caddy?
- Automatic HTTPS via Let's Encrypt (or self-signed for testing)
- Simple configuration
- Built-in
forward_authdirective - HTTP/2 and HTTP/3 support
- Zero-downtime config reloads
Key Functions:
- TLS Termination: Handles HTTPS, obtains/renews certificates
- Routing: Routes requests to correct backend service
- Auth Enforcement: Forward-auth for wiki and lldap
- Header Injection: Adds
Remote-Userto backend requests
Snippet Pattern:
We use Caddy's snippet feature for DRY configuration:
(auth) {
forward_auth authelia:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
}
wiki.example.com {
import auth # Reuse auth snippet
reverse_proxy jspwiki:8080
}
Security Model
Hardened Network Architecture
Exposed Ports (Minimal Attack Surface):
80/443(HTTP/HTTPS) - Caddy reverse proxy only2222(SSH) - Git operations only- All services: Zero direct external access
Port Matrix:
| Service | Internal Port | External Access | Authentication |
|---|---|---|---|
| Caddy | 80, 443 | ✅ Public | N/A (proxy) |
| Gitea SSH | 22 → 2222 | ✅ Public | SSH keys |
| Authelia | 9091 | ❌ Via Caddy | None (login service) |
| lldap Web | 17170 | ❌ Via Caddy | Authelia + lldap password |
| lldap LDAP | 3890 | ❌ Internal only | LDAP bind |
| Gitea Web | 3000 | ❌ Via Caddy | OIDC (Authelia) |
| JSPWiki | 8080 | ❌ Via Caddy | Forward-Auth (Authelia) |
| Registration | 5000 | ❌ Via Caddy | Mixed (public form + auth admin) |
Defense in Depth
Layer 1: Network Isolation
- All services on internal Docker bridge network (
org-network) - Services communicate via Docker DNS hostnames (e.g.,
lldap:3890,authelia:9091) - Zero direct port exposure except Caddy (80/443) and Git SSH (2222)
- No localhost or 127.0.0.1 bindings
Layer 2: Reverse Proxy (Caddy)
- Single entry point for all web traffic
- TLS 1.3 with automatic certificate management
- HTTP → HTTPS redirect (except ACME challenges)
- Forward-auth integration with Authelia
Layer 3: Authentication (Authelia)
- Centralized authentication for all services
- LDAP credential validation via lldap
- Session management with secure cookies
- Failed attempt tracking and auto-ban
Layer 4: Authorization (Authelia)
- Domain-based access policies
- Group-based permissions (lldap_admin)
- Admin-only access controls
Layer 5: Multi-Factor Authentication
- TOTP (Time-based One-Time Password)
- Per-user 2FA registration
- Policy-enforced (configurable)
Layer 6: Application-Level Security
- Gitea: OIDC token validation + internal OIDC auth
- JSPWiki: Container auth (trusts Remote-User header)
- lldap: Admin password required after Authelia auth
- Registration: Public form (unauthenticated) + protected admin dashboard
Secret Management
File-Based Secrets (Docker Best Practice):
Instead of environment variables, we use Docker secret files:
# compose.yml
authelia:
environment:
- AUTHELIA_SESSION_SECRET_FILE=/secrets/SESSION_SECRET
volumes:
- ./secrets/authelia:/secrets:ro # Read-only mount
Benefits:
- Not visible in
docker inspect - Not visible in process listing (
ps aux) - Individual file permissions (600)
- Easier to rotate (replace file, restart container)
- Not logged accidentally
Secret Types:
- Symmetric Keys: SESSION_SECRET, STORAGE_ENCRYPTION_KEY (AES-256)
- HMAC Keys: OIDC_HMAC_SECRET, JWT_SECRET (SHA-256)
- Asymmetric Keys: OIDC_PRIVATE_KEY (RSA-2048)
- Passwords: LDAP_USER_PASS (bcrypt/SSHA)
Generation:
# Strong random secrets
openssl rand -hex 32
# RSA key pair
authelia crypto pair rsa generate --bits 2048
Session Security
Session Lifecycle:
- User authenticates → Authelia creates session
- Session stored in SQLite with unique ID
- Cookie sent to user:
authelia_session=<id> - Cookie attributes:
HttpOnly: Not accessible via JavaScriptSecure: Only sent over HTTPSSameSite=Lax: CSRF protectionDomain=.example.com: Shared across subdomains
Session Expiration:
SESSION_EXPIRATION: Absolute lifetime (e.g., 1h)SESSION_INACTIVITY: Idle timeout (e.g., 5m)SESSION_REMEMBER_ME: Extended lifetime if opted in (e.g., 1M)
Session Invalidation:
- Logout: Immediate deletion
- Password change: All sessions invalidated
- Inactivity timeout: Automatic cleanup
Network Architecture
Docker Network Topology:
External Internet
│
│ :80, :443
▼
┌───────────┐
│ Caddy │ (ports published)
└─────┬─────┘
│ org-network (172.x.0.0/16)
│
├─────► lldap:17170 (web UI)
├─────► lldap:3890 (LDAP)
├─────► authelia:9091
├─────► gitea:3000
└─────► jspwiki:8080
Service Discovery:
Docker's internal DNS resolves service names:
lldap→ container IP (e.g., 172.19.0.2)authelia→ container IP (e.g., 172.19.0.3)- etc.
Why this matters:
- Services use service names in config (e.g.,
ldap://lldap:3890) - Works in any environment (local, cloud, etc.)
- No hardcoded IPs
- Automatic failover if container restarts
Data Flow Examples
Example 1: First-Time Login to Wiki
1. User: Navigate to https://wiki.example.com
2. Browser → Caddy: GET /
3. Caddy → Authelia: Forward-auth subrequest
4. Authelia: No session cookie → 401 Unauthorized
5. Caddy → Browser: 302 Redirect to https://auth.example.com?rd=...
6. Browser → Authelia: GET /login?rd=...
7. Authelia → Browser: Login form
8. User: Enter username + password
9. Browser → Authelia: POST /login (credentials)
10. Authelia → lldap: LDAP BIND (validate password)
11. lldap → Authelia: BIND successful
12. Authelia → Browser: TOTP form (2FA)
13. User: Enter TOTP code from authenticator app
14. Browser → Authelia: POST /2fa (TOTP code)
15. Authelia: Validate TOTP, create session
16. Authelia → Browser: 302 Redirect to wiki + session cookie
17. Browser → Caddy: GET / (with session cookie)
18. Caddy → Authelia: Forward-auth subrequest (with cookie)
19. Authelia: Validate session → 200 OK + Remote-User: alice
20. Caddy → JSPWiki: GET / (with Remote-User header)
21. JSPWiki: Trust Remote-User, render page as alice
22. JSPWiki → Caddy: HTML response
23. Caddy → Browser: HTML response
24. User: Sees wiki page, logged in as alice
Example 2: Gitea SSO via OIDC
1. User: Click "Sign in with OpenID Connect" on Gitea
2. Gitea → Browser: Redirect to Authelia authorize endpoint
3. Browser → Authelia: GET /api/oidc/authorization?client_id=gitea&...
4. Authelia: Check session → Valid (from wiki login earlier)
5. Authelia: Check consent → Not required (trusted client)
6. Authelia → Browser: Redirect to Gitea callback with code
7. Browser → Gitea: GET /callback?code=abc123
8. Gitea → Authelia: POST /api/oidc/token (exchange code for tokens)
9. Authelia: Validate code, issue access_token + id_token
10. Authelia → Gitea: Tokens (JWT)
11. Gitea: Decode id_token, extract user info
12. Gitea: Create/update user account for alice
13. Gitea: Create Gitea session
14. Gitea → Browser: Set Gitea session cookie, redirect to dashboard
15. User: Logged into Gitea without entering credentials again (SSO!)
Design Decisions
Why File-Based Secrets?
Decision: Use individual files for secrets instead of environment variables.
Rationale:
- Security: Not visible in
docker inspector process listing - Auditability: File permissions and access can be monitored
- Rotation: Easy to update (replace file + restart)
- Best Practice: Recommended by Docker, Kubernetes
Trade-off: Slightly more complex volume mounts, but worth it for security.
Why Both OIDC and Forward-Auth?
Decision: Use OIDC for Gitea, forward-auth for JSPWiki/lldap.
Rationale:
- Native Support: Gitea has excellent OIDC support, provides better UX
- SSO Experience: OIDC gives true SSO, no credential re-entry
- Compatibility: JSPWiki doesn't support OIDC, forward-auth is universal
- Flexibility: Demonstrates both patterns for learning purposes
Trade-off: Two authentication flows to understand, but appropriate for each service.
Why JSPWiki LDAP Sync?
Decision: Synchronize LDAP users to JSPWiki's XML database on startup.
Rationale:
- Permission Model: JSPWiki's permission system needs users in its database
- Group Mapping: Allows mapping LDAP groups to JSPWiki roles
- Offline Operation: JSPWiki can function if LDAP is temporarily down
Trade-off: Users must be synced before they can access wiki, but this happens automatically on container start.
Why SQLite for Production?
Decision: Use SQLite for Gitea and Authelia databases (optional PostgreSQL upgrade path).
Rationale:
- Simplicity: No external database to manage
- Sufficient for Small Orgs: Handles thousands of users easily
- Backup Simplicity: Single file to backup
- Zero Config: Works out of the box
Trade-off:
- For large installations (>10k repos, >100 concurrent users), PostgreSQL recommended
- Easy migration path available if needed
Why Automatic LDAP_BASE_DN Derivation?
Decision: Auto-generate dc=example,dc=com from BASE_DOMAIN=example.com.
Rationale:
- DRY: Don't repeat yourself - one config value
- Fewer Errors: Less to configure = fewer mistakes
- Convention: Standard LDAP DN format from domain
Trade-off: Less flexibility if you need non-standard DN, but can override with manual setting.
Performance Considerations
Bottlenecks
-
Authelia LDAP Lookups: Each login queries LDAP
- Mitigation: Authelia caches results per session
- Scale: LDAP is read-optimized, handles thousands of auth/sec
-
OIDC Token Validation: Gitea validates tokens on each request
- Mitigation: JWTs are stateless, validation is local
- Scale: CPU-bound, scales horizontally
-
SQLite Lock Contention: Multiple writers can cause locking
- Mitigation: WAL mode enabled by default
- Scale: If bottleneck appears, migrate to PostgreSQL
Optimization Recommendations
For Heavy Load:
- Deploy Redis for Authelia session storage (instead of SQLite)
- Use PostgreSQL for Gitea database
- Enable HTTP/3 (QUIC) in Caddy
- Increase Authelia session cache size
- Deploy multiple Authelia instances behind load balancer
For Low Latency:
- Enable HTTP/2 server push in Caddy
- Tune session expiration for use case
- Use CDN for static assets (Gitea avatars, etc.)
Monitoring and Observability
Logs: All containers log to stdout/stderr, accessible via:
docker compose logs -f [service]
Metrics:
- Authelia exposes Prometheus metrics on
/metrics - Caddy exposes metrics via admin API
- Gitea has built-in metrics endpoint
Recommended Monitoring:
- Container health checks (Authelia provides
/api/health) - Certificate expiration monitoring (Caddy auto-renews, but monitor anyway)
- Failed login attempts (Authelia logs)
- Session counts (Authelia database)
- Repository count/size (Gitea admin panel)
Disaster Recovery
Backup Strategy:
Critical Data:
lldap_data- User database (highest priority)gitea_data- All repositoriesjspwiki_data- Wiki contentauthelia_data- Sessions, 2FA registrationscaddy_data- TLS certificatessecrets/directory - All secrets
Recovery Procedure:
- Restore
secrets/directory - Restore Docker volumes from backup
- Deploy stack:
./deploy.sh - Verify all services start
- Test authentication flow
RPO (Recovery Point Objective): How much data loss is acceptable?
- Recommended: Daily backups of volumes
- Critical orgs: Hourly backups + off-site replication
RTO (Recovery Time Objective): How quickly to recover?
- With backups: ~15 minutes
- From scratch (users recreate accounts): Several hours
Conclusion
This architecture demonstrates modern authentication and authorization patterns using open-source components. The system is designed for:
- Security: Defense in depth, modern protocols
- Usability: Single sign-on, familiar UX
- Maintainability: Simple deployment, clear structure
- Scalability: Can grow from 10 to 10,000 users with appropriate tuning
For questions or improvements, see the main README.md or open an issue on GitHub.