1106 lines
45 KiB
Markdown
1106 lines
45 KiB
Markdown
# 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](#system-architecture)
|
|
- [Authentication Protocols](#authentication-protocols)
|
|
- [LDAP Integration](#ldap-integration)
|
|
- [OIDC Flow](#oidc-flow)
|
|
- [Forward-Auth Pattern](#forward-auth-pattern)
|
|
- [Component Details](#component-details)
|
|
- [Security Model](#security-model)
|
|
- [Network Architecture](#network-architecture)
|
|
- [Data Flow Examples](#data-flow-examples)
|
|
- [Design Decisions](#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:**
|
|
|
|
1. Authelia connects to lldap using bind credentials:
|
|
```
|
|
BIND uid=admin,ou=people,dc=example,dc=com
|
|
PASSWORD: <from secrets/lldap/LDAP_USER_PASS>
|
|
```
|
|
|
|
2. User attempts login with username "alice"
|
|
|
|
3. Authelia performs LDAP search:
|
|
```ldap
|
|
SEARCH base_dn: ou=people,dc=example,dc=com
|
|
FILTER: (&(uid=alice)(objectClass=person))
|
|
```
|
|
|
|
4. Authelia validates password by attempting bind as user:
|
|
```
|
|
BIND uid=alice,ou=people,dc=example,dc=com
|
|
PASSWORD: <user's password>
|
|
```
|
|
|
|
5. If successful, Authelia queries groups:
|
|
```ldap
|
|
SEARCH base_dn: ou=groups,dc=example,dc=com
|
|
FILTER: (member=uid=alice,ou=people,dc=example,dc=com)
|
|
```
|
|
|
|
**References:**
|
|
- [RFC 4511 - LDAP Protocol](https://tools.ietf.org/html/rfc4511)
|
|
- [lldap Documentation](https://github.com/lldap/lldap)
|
|
|
|
### 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):**
|
|
|
|
```json
|
|
{
|
|
"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:**
|
|
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
|
|
- [OAuth 2.0 Authorization Code Flow](https://tools.ietf.org/html/rfc6749#section-4.1)
|
|
- [Authelia OIDC Documentation](https://www.authelia.com/integration/openid-connect/introduction/)
|
|
|
|
### 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:**
|
|
|
|
```caddyfile
|
|
# 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 email
|
|
- `Remote-Name`: Display name
|
|
|
|
**JSPWiki Container Authentication:**
|
|
|
|
JSPWiki is configured to trust the `Remote-User` header via our custom `RemoteUserFilter`:
|
|
|
|
```java
|
|
// 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:
|
|
1. All services on internal Docker network
|
|
2. Only Caddy exposes external ports
|
|
3. JSPWiki port 8080 not exposed to host
|
|
|
|
**References:**
|
|
- [Authelia Forward-Auth Docs](https://www.authelia.com/integration/proxies/fowarded-headers/)
|
|
- [Caddy forward_auth](https://caddyserver.com/docs/caddyfile/directives/forward_auth)
|
|
|
|
## 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:**
|
|
1. **Multi-factor Authentication**: TOTP (RFC 6238), WebAuthn planned
|
|
2. **Single Sign-On**: One login session across all services
|
|
3. **Access Control**: Domain-based policies (bypass, one_factor, two_factor)
|
|
4. **Session Management**: Configurable lifespans, remember-me
|
|
5. **Rate Limiting**: Brute-force protection
|
|
|
|
**Configuration Structure:**
|
|
```yaml
|
|
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:**
|
|
```ini
|
|
[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:**
|
|
1. Caddy enforces authentication via forward-auth
|
|
2. Caddy passes `Remote-User` header
|
|
3. Our custom `RemoteUserFilter` servlet filter wraps the request
|
|
4. 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:
|
|
1. Queries lldap via `ldapsearch`
|
|
2. Generates `userdatabase.xml` with all LDAP users
|
|
3. Maps `lldap_admin` group to JSPWiki's `Admin` group
|
|
4. 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**:
|
|
1. **Public Route** (`/`) - Registration form (no auth required)
|
|
2. **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:**
|
|
|
|
```sql
|
|
-- 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`:
|
|
|
|
```python
|
|
# 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=register`
|
|
- `REGISTRATION_ADMIN_EMAIL` - For admin notifications
|
|
- `SMTP_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:**
|
|
|
|
1. Review pending requests at `/admin`
|
|
2. Approve → User created in lldap automatically
|
|
3. Reject → Logged to audit trail
|
|
4. Manage active users in lldap web interface
|
|
|
|
**API Endpoints:**
|
|
|
|
- `GET /admin` - View pending requests and audit log
|
|
- `POST /admin/approve/{id}` - Create user in lldap, log approval
|
|
- `POST /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_auth` directive
|
|
- HTTP/2 and HTTP/3 support
|
|
- Zero-downtime config reloads
|
|
|
|
**Key Functions:**
|
|
1. **TLS Termination**: Handles HTTPS, obtains/renews certificates
|
|
2. **Routing**: Routes requests to correct backend service
|
|
3. **Auth Enforcement**: Forward-auth for wiki and lldap
|
|
4. **Header Injection**: Adds `Remote-User` to backend requests
|
|
|
|
**Snippet Pattern:**
|
|
|
|
We use Caddy's snippet feature for DRY configuration:
|
|
```caddyfile
|
|
(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 only
|
|
- `2222` (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:
|
|
|
|
```yaml
|
|
# 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:**
|
|
```bash
|
|
# Strong random secrets
|
|
openssl rand -hex 32
|
|
|
|
# RSA key pair
|
|
authelia crypto pair rsa generate --bits 2048
|
|
```
|
|
|
|
### Session Security
|
|
|
|
**Session Lifecycle:**
|
|
1. User authenticates → Authelia creates session
|
|
2. Session stored in SQLite with unique ID
|
|
3. Cookie sent to user: `authelia_session=<id>`
|
|
4. Cookie attributes:
|
|
- `HttpOnly`: Not accessible via JavaScript
|
|
- `Secure`: Only sent over HTTPS
|
|
- `SameSite=Lax`: CSRF protection
|
|
- `Domain=.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 inspect` or 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
|
|
|
|
1. **Authelia LDAP Lookups**: Each login queries LDAP
|
|
- *Mitigation*: Authelia caches results per session
|
|
- *Scale*: LDAP is read-optimized, handles thousands of auth/sec
|
|
|
|
2. **OIDC Token Validation**: Gitea validates tokens on each request
|
|
- *Mitigation*: JWTs are stateless, validation is local
|
|
- *Scale*: CPU-bound, scales horizontally
|
|
|
|
3. **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:**
|
|
1. Deploy Redis for Authelia session storage (instead of SQLite)
|
|
2. Use PostgreSQL for Gitea database
|
|
3. Enable HTTP/3 (QUIC) in Caddy
|
|
4. Increase Authelia session cache size
|
|
5. Deploy multiple Authelia instances behind load balancer
|
|
|
|
**For Low Latency:**
|
|
1. Enable HTTP/2 server push in Caddy
|
|
2. Tune session expiration for use case
|
|
3. Use CDN for static assets (Gitea avatars, etc.)
|
|
|
|
## Monitoring and Observability
|
|
|
|
**Logs:**
|
|
All containers log to stdout/stderr, accessible via:
|
|
```bash
|
|
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:**
|
|
1. Container health checks (Authelia provides `/api/health`)
|
|
2. Certificate expiration monitoring (Caddy auto-renews, but monitor anyway)
|
|
3. Failed login attempts (Authelia logs)
|
|
4. Session counts (Authelia database)
|
|
5. Repository count/size (Gitea admin panel)
|
|
|
|
## Disaster Recovery
|
|
|
|
**Backup Strategy:**
|
|
|
|
**Critical Data:**
|
|
1. `lldap_data` - User database (highest priority)
|
|
2. `gitea_data` - All repositories
|
|
3. `jspwiki_data` - Wiki content
|
|
4. `authelia_data` - Sessions, 2FA registrations
|
|
5. `caddy_data` - TLS certificates
|
|
6. `secrets/` directory - All secrets
|
|
|
|
**Recovery Procedure:**
|
|
1. Restore `secrets/` directory
|
|
2. Restore Docker volumes from backup
|
|
3. Deploy stack: `./deploy.sh`
|
|
4. Verify all services start
|
|
5. 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](./README.md) or open an issue on GitHub.
|