573 lines
18 KiB
Markdown
573 lines
18 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to AI assistants (like Claude Code) when working with this repository.
|
|
|
|
## Project Overview
|
|
|
|
**Project Type**: Self-hosted authentication and collaboration platform
|
|
**Primary Language**: Shell (deployment), YAML (configuration), Java (JSPWiki customization)
|
|
**Architecture**: Docker Compose multi-container application
|
|
**Deployment Model**: Remote deployment via SSH/rsync from local machine
|
|
|
|
### Core Services
|
|
1. **lldap** - LDAP user directory (Rust application, pre-built image)
|
|
2. **Authelia** - SSO/2FA server (Go application, pre-built image)
|
|
3. **Gitea** - Git hosting (Go application, pre-built image)
|
|
4. **JSPWiki** - Wiki platform (Java/Tomcat, custom Docker image)
|
|
5. **Caddy** - Reverse proxy (Go application, pre-built image)
|
|
|
|
## Project Philosophy
|
|
|
|
### Single Source of Truth
|
|
- `.env` file is the **only** place users configure the system
|
|
- All service configs are generated from `.env` via templates
|
|
- **Never** require users to edit multiple config files
|
|
- **Never** hardcode values that should be configurable
|
|
|
|
### Zero Manual Steps
|
|
- `./deploy.sh` should handle everything from secrets to deployment
|
|
- User should only need to edit `.env` and run one command
|
|
- Secrets auto-generated if missing
|
|
- Configs auto-generated from templates
|
|
- Service dependencies handled automatically
|
|
|
|
### Security First
|
|
- File-based secrets (not environment variables)
|
|
- Read-only mounts where possible
|
|
- Secrets never committed to git
|
|
- Modern authentication protocols (LDAP, OIDC, Forward-Auth)
|
|
- Defense in depth architecture
|
|
|
|
### Educational Value
|
|
- Code should teach authentication patterns
|
|
- Comments explain "why" not just "what"
|
|
- Documentation references RFCs and specs
|
|
- Architecture demonstrates industry best practices
|
|
|
|
## File Structure and Responsibilities
|
|
|
|
```
|
|
org-stack/
|
|
├── .env.example # Configuration template (edit this to add new options)
|
|
├── .env # User configuration (generated, not in git)
|
|
├── .gitignore # Ensures secrets/ and .env not committed
|
|
│
|
|
├── deploy.sh # Main deployment script
|
|
│ ├── Checks/creates .env
|
|
│ ├── Generates secrets if missing
|
|
│ ├── Derives LDAP_BASE_DN from BASE_DOMAIN
|
|
│ ├── Hashes Gitea OIDC secret
|
|
│ ├── Generates configs from templates
|
|
│ └── Rsyncs to remote server and starts services
|
|
│
|
|
├── manage.sh # Management operations on remote server
|
|
│ ├── logs, restart, update
|
|
│ ├── backup, restore
|
|
│ └── status, reset
|
|
│
|
|
├── compose.yml # Docker Compose service definitions
|
|
│ ├── Service configs with educational comments
|
|
│ ├── Volume mounts (configs, secrets, data)
|
|
│ └── Network and dependency definitions
|
|
│
|
|
├── Caddyfile.production.template # Caddy config for Let's Encrypt
|
|
├── Caddyfile.test.template # Caddy config for self-signed certs
|
|
│ └── Generated to Caddyfile by deploy.sh based on USE_SELF_SIGNED_CERTS
|
|
│
|
|
├── authelia/
|
|
│ └── configuration.yml.template # Authelia config template
|
|
│ ├── Uses modern v4.38+ syntax (no deprecation warnings)
|
|
│ ├── References secrets via _FILE environment variables
|
|
│ └── Generated to configuration.yml by deploy.sh
|
|
│
|
|
├── jspwiki/
|
|
│ ├── Dockerfile # Custom JSPWiki image with SSO support
|
|
│ ├── RemoteUserFilter.java # Servlet filter for container auth
|
|
│ ├── ldap-sync.sh # Syncs lldap users to JSPWiki XML
|
|
│ ├── configure-web-xml.sh # Modifies web.xml for RemoteUserFilter
|
|
│ └── entrypoint.sh # Container startup: sync LDAP, start Tomcat
|
|
│
|
|
├── jspwiki-custom.properties # JSPWiki config (container auth enabled)
|
|
├── jspwiki.policy # JSPWiki security policy
|
|
│
|
|
├── secrets/ # Auto-generated secrets (NEVER commit)
|
|
│ ├── lldap/
|
|
│ │ ├── JWT_SECRET
|
|
│ │ └── LDAP_USER_PASS
|
|
│ └── authelia/
|
|
│ ├── JWT_SECRET
|
|
│ ├── SESSION_SECRET
|
|
│ ├── STORAGE_ENCRYPTION_KEY
|
|
│ ├── OIDC_HMAC_SECRET
|
|
│ └── OIDC_PRIVATE_KEY
|
|
│
|
|
└── Documentation
|
|
├── README.md # User-facing documentation and quick start
|
|
├── ARCHITECTURE.md # Technical deep-dive
|
|
└── CLAUDE.md # This file (AI assistant guidance)
|
|
```
|
|
|
|
## Security Considerations
|
|
|
|
### LDAP Injection Prevention
|
|
|
|
The registration service handles user input that is used in LDAP operations. Always follow these practices:
|
|
|
|
**Input Validation (Defense Layer 1):**
|
|
- Username: Strict validation - alphanumeric + underscore only, must start with letter, 2-64 chars
|
|
- Email: Basic format validation
|
|
- Names: Length limits (max 100 characters)
|
|
- Apply validation at form submission AND before LDAP operations (defense in depth)
|
|
|
|
**LDAP DN Escaping (Defense Layer 2):**
|
|
```python
|
|
def escape_ldap_dn(value: str) -> str:
|
|
"""Escape LDAP DN special characters per RFC 4514"""
|
|
# Escape: , \ # + < > ; " =
|
|
# Also escape leading/trailing spaces
|
|
```
|
|
|
|
**Always escape when constructing DNs:**
|
|
```python
|
|
# BAD - Vulnerable to injection
|
|
user_dn = f'uid={username},ou=people,dc=example,dc=com'
|
|
|
|
# GOOD - Escaped and validated
|
|
if not validate_username_strict(username):
|
|
raise ValueError("Invalid username")
|
|
escaped_username = escape_ldap_dn(username)
|
|
user_dn = f'uid={escaped_username},ou=people,dc=example,dc=com'
|
|
```
|
|
|
|
**LDAP Filter Escaping:**
|
|
If you add ldapsearch operations, escape filter values:
|
|
```python
|
|
def escape_ldap_filter(value: str) -> str:
|
|
"""Escape LDAP filter special characters"""
|
|
# Escape: * ( ) \ NULL
|
|
value = value.replace('\\', '\\5c')
|
|
value = value.replace('*', '\\2a')
|
|
value = value.replace('(', '\\28')
|
|
value = value.replace(')', '\\29')
|
|
value = value.replace('\x00', '\\00')
|
|
return value
|
|
```
|
|
|
|
### SQL Injection Prevention
|
|
|
|
Always use parameterized queries:
|
|
```python
|
|
# GOOD - Parameterized
|
|
db.execute('SELECT * FROM users WHERE username = ?', (username,))
|
|
|
|
# BAD - String interpolation
|
|
db.execute(f'SELECT * FROM users WHERE username = "{username}"')
|
|
```
|
|
|
|
### GraphQL Security
|
|
|
|
The lldap GraphQL API is safe from injection because it uses parameterized variables. Always pass user input as variables, never in the query string:
|
|
```python
|
|
# GOOD - Parameterized variables
|
|
variables = {'user': {'id': username, 'email': email}}
|
|
response = client.post(url, json={'query': mutation, 'variables': variables})
|
|
|
|
# BAD - String interpolation in query
|
|
query = f'mutation {{ createUser(id: "{username}") }}' # Vulnerable!
|
|
```
|
|
|
|
## Common Tasks
|
|
|
|
### Adding a New Configuration Option
|
|
|
|
1. **Add to `.env.example`** with clear comments and default value
|
|
2. **Update template file** (Caddyfile or authelia config) to use `${NEW_VAR}`
|
|
3. **Update `deploy.sh`** to export the variable for envsubst
|
|
4. **Update documentation** (README.md) if user-facing
|
|
5. **Test** by removing .env and running deploy.sh
|
|
|
|
Example:
|
|
```bash
|
|
# .env.example
|
|
NEW_FEATURE_ENABLED=true # Enable new feature
|
|
|
|
# authelia/configuration.yml.template
|
|
new_feature:
|
|
enabled: ${NEW_FEATURE_ENABLED}
|
|
|
|
# deploy.sh (in export section)
|
|
export NEW_FEATURE_ENABLED
|
|
```
|
|
|
|
### Adding a New Service
|
|
|
|
1. **Add service to `compose.yml`** with clear comments
|
|
2. **Add subdomain to `.env.example`** (e.g., `NEWSERVICE_SUBDOMAIN=new`)
|
|
3. **Add to Caddyfile templates** with appropriate auth config
|
|
4. **Update `deploy.sh`** summary section to show new URL
|
|
5. **Update `manage.sh`** if service needs special handling
|
|
6. **Document in README.md** and ARCHITECTURE.md
|
|
|
|
### Modifying Authelia Configuration
|
|
|
|
**Important**: Always use **modern v4.38+ syntax**. Check [Authelia docs](https://www.authelia.com/configuration/prologue/introduction/) for latest syntax.
|
|
|
|
Common deprecations to avoid:
|
|
- ❌ `username_attribute` → ✅ `attributes.username`
|
|
- ❌ `access_token_lifespan` → ✅ `lifespans.access_token`
|
|
- ❌ `issuer_private_key: |` → ✅ Use `*_FILE` env var
|
|
|
|
When adding new features:
|
|
1. Check if Authelia exposes secret via `*_FILE` env variable
|
|
2. If yes, use file-based secret (add to deploy.sh generation)
|
|
3. If no, use template variable from .env
|
|
|
|
### Working with Secrets
|
|
|
|
**Adding a new secret:**
|
|
|
|
```bash
|
|
# 1. Add generation to deploy.sh
|
|
generate_secret_file "secrets/authelia/NEW_SECRET" 32
|
|
|
|
# 2. Add environment variable to compose.yml
|
|
environment:
|
|
- AUTHELIA_NEW_SECRET_FILE=/secrets/NEW_SECRET
|
|
|
|
# 3. Add volume mount if in new directory
|
|
volumes:
|
|
- ./secrets/authelia:/secrets:ro
|
|
|
|
# 4. Reference in template with comment
|
|
# authelia/configuration.yml.template
|
|
# new_secret read from AUTHELIA_NEW_SECRET_FILE
|
|
```
|
|
|
|
**Never:**
|
|
- ❌ Commit secrets to git
|
|
- ❌ Put secrets in environment variables (use _FILE pattern)
|
|
- ❌ Echo secrets in logs
|
|
- ❌ Use predictable secret values
|
|
|
|
### Debugging Authentication Issues
|
|
|
|
**Check in this order:**
|
|
|
|
1. **Service Status**
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose ps'
|
|
```
|
|
All services should be "Up" and "healthy" (Authelia)
|
|
|
|
2. **Authelia Logs**
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose logs authelia | tail -50'
|
|
```
|
|
Look for:
|
|
- Configuration errors (missing secrets, bad syntax)
|
|
- LDAP connection errors
|
|
- Authentication failures
|
|
|
|
3. **Secret Files**
|
|
```bash
|
|
ssh user@host 'ls -la ~/org-stack/secrets/authelia/'
|
|
```
|
|
All files should exist with 600 permissions
|
|
|
|
4. **Network Connectivity**
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose exec gitea curl http://authelia:9091/.well-known/openid-configuration'
|
|
```
|
|
Should return JSON (OIDC discovery document)
|
|
|
|
5. **LDAP Connectivity**
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose exec authelia ldapsearch -H ldap://lldap:3890 -D "uid=admin,ou=people,dc=example,dc=com" -w "$(cat secrets/lldap/LDAP_USER_PASS)" -b "dc=example,dc=com" "(uid=admin)"'
|
|
```
|
|
Should return admin user entry
|
|
|
|
### Common Error Patterns
|
|
|
|
**"502 Bad Gateway" on all services except Gitea**
|
|
- **Cause**: Authelia container not running or crashing
|
|
- **Check**: `docker compose logs authelia`
|
|
- **Common root cause**: Missing or incorrect secret file path
|
|
|
|
**"No such host" errors in Caddy logs**
|
|
- **Cause**: Docker network issues or service not started
|
|
- **Fix**: `docker compose down && docker compose up -d`
|
|
|
|
**"Certificate signed by unknown authority" in Gitea**
|
|
- **Cause**: Gitea trying to use external HTTPS URL with self-signed cert
|
|
- **Fix**: Use internal URL in OIDC config: `http://authelia:9091/...`
|
|
|
|
**Authelia deprecation warnings in logs**
|
|
- **Cause**: Using old configuration syntax
|
|
- **Fix**: Update `authelia/configuration.yml.template` to modern syntax
|
|
- **Reference**: [Authelia Migration Guides](https://www.authelia.com/reference/guides/migration/)
|
|
|
|
## Code Style Guidelines
|
|
|
|
### Shell Scripts (deploy.sh, manage.sh)
|
|
|
|
```bash
|
|
# Good: Clear error messages with context
|
|
if [ ! -f .env ]; then
|
|
error ".env file not found. Copy .env.example to .env and configure."
|
|
fi
|
|
|
|
# Good: Functions with clear names
|
|
generate_secret_file() {
|
|
local file_path=$1
|
|
local length=${2:-32}
|
|
# ...
|
|
}
|
|
|
|
# Good: Comments explain "why"
|
|
# Derive LDAP_BASE_DN from BASE_DOMAIN because most users won't know LDAP DN format
|
|
if [ "$LDAP_BASE_DN" = "AUTO" ]; then
|
|
DERIVED_DN=$(echo "$BASE_DOMAIN" | sed 's/\./,dc=/g' | sed 's/^/dc=/')
|
|
# ...
|
|
fi
|
|
```
|
|
|
|
### YAML Configuration
|
|
|
|
```yaml
|
|
# Good: Comments explain purpose and link to docs
|
|
authentication_backend:
|
|
ldap:
|
|
# lldap connection settings
|
|
# See: https://github.com/lldap/lldap
|
|
address: 'ldap://lldap:3890'
|
|
|
|
# Good: Group related settings
|
|
lifespans:
|
|
access_token: ${ACCESS_TOKEN_LIFESPAN}
|
|
authorize_code: ${AUTHORIZE_CODE_LIFESPAN}
|
|
id_token: ${ID_TOKEN_LIFESPAN}
|
|
refresh_token: ${REFRESH_TOKEN_LIFESPAN}
|
|
```
|
|
|
|
### Docker Compose
|
|
|
|
```yaml
|
|
# Good: Comments explain auth method and purpose
|
|
gitea:
|
|
# Self-hosted Git service
|
|
# Uses OIDC to authenticate users through Authelia (SSO)
|
|
image: gitea/gitea:latest
|
|
environment:
|
|
- GITEA__server__DOMAIN=${GITEA_SUBDOMAIN}.${BASE_DOMAIN}
|
|
volumes:
|
|
- gitea_data:/data # Git repos, database, config
|
|
```
|
|
|
|
## Testing Approach
|
|
|
|
### Before Committing Changes
|
|
|
|
1. **Test fresh deployment**:
|
|
```bash
|
|
rm .env
|
|
rm -rf secrets/
|
|
./deploy.sh
|
|
```
|
|
|
|
2. **Verify services start**:
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose ps'
|
|
```
|
|
All should be "Up"
|
|
|
|
3. **Test authentication flow**:
|
|
- Create user in lldap
|
|
- Login to wiki (tests Forward-Auth + 2FA)
|
|
- Login to Gitea (tests OIDC + 2FA)
|
|
|
|
4. **Check for deprecation warnings**:
|
|
```bash
|
|
ssh user@host 'cd ~/org-stack && docker compose logs authelia' | grep -i deprecat
|
|
```
|
|
Should be zero warnings
|
|
|
|
5. **Verify secrets not in git**:
|
|
```bash
|
|
git status
|
|
```
|
|
secrets/ and .env should not appear
|
|
|
|
### Test Scenarios
|
|
|
|
**Scenario 1: First-time deployment**
|
|
- User has never deployed before
|
|
- Should complete with zero manual steps
|
|
- All secrets auto-generated
|
|
|
|
**Scenario 2: Configuration change**
|
|
- User changes BASE_DOMAIN in .env
|
|
- Runs deploy.sh
|
|
- All services update to new domain
|
|
|
|
**Scenario 3: Secret rotation**
|
|
- Delete a secret file
|
|
- Run deploy.sh
|
|
- Secret regenerated, services restart
|
|
|
|
**Scenario 4: Disaster recovery**
|
|
- Backup volumes with manage.sh backup
|
|
- Destroy everything with manage.sh reset
|
|
- Restore from backup
|
|
- Everything works
|
|
|
|
## Understanding the Authentication Flow
|
|
|
|
### When User Accesses Wiki
|
|
|
|
```
|
|
1. Browser → Caddy: GET https://wiki.example.com/
|
|
2. Caddy → Authelia: Forward-auth subrequest to /api/authz/forward-auth
|
|
3. Authelia checks session cookie:
|
|
a. If valid session: Return 200 + Remote-User header
|
|
b. If no session: Return 401 + redirect to login
|
|
4. If 401:
|
|
- Caddy → Browser: 302 to Authelia login
|
|
- User enters credentials
|
|
- Authelia → lldap: Validate password via LDAP BIND
|
|
- Authelia prompts for TOTP (if REQUIRE_2FA=true)
|
|
- Authelia creates session, sets cookie
|
|
- Authelia → Browser: 302 back to wiki
|
|
5. Caddy forwards request to JSPWiki with Remote-User header
|
|
6. JSPWiki trusts Remote-User (via RemoteUserFilter)
|
|
7. User sees wiki page as authenticated user
|
|
```
|
|
|
|
### When User Accesses Gitea
|
|
|
|
```
|
|
1. Browser → Gitea: Click "Sign in with OpenID Connect"
|
|
2. Gitea → Browser: 302 to Authelia /api/oidc/authorization
|
|
3. Authelia checks session:
|
|
a. If valid: Skip to step 5
|
|
b. If no session: Show login + 2FA
|
|
4. User authenticates (same as wiki flow)
|
|
5. Authelia → Browser: 302 to Gitea callback with code
|
|
6. Gitea → Authelia: POST /api/oidc/token with code
|
|
7. Authelia → Gitea: Access token + ID token (JWT)
|
|
8. Gitea validates JWT, extracts user info
|
|
9. Gitea creates local session
|
|
10. User logged into Gitea (SSO - no credential re-entry if already logged into wiki)
|
|
```
|
|
|
|
## When to Ask User for Clarification
|
|
|
|
**Always ask before:**
|
|
- Changing authentication behavior (2FA, SSO, etc.)
|
|
- Modifying security-related code
|
|
- Breaking changes to .env configuration
|
|
- Changing default behavior
|
|
- Adding new external dependencies
|
|
|
|
**No need to ask for:**
|
|
- Bug fixes (broken features)
|
|
- Documentation improvements
|
|
- Code cleanup/refactoring (no behavior change)
|
|
- Adding educational comments
|
|
- Fixing deprecation warnings
|
|
|
|
## Important Constraints
|
|
|
|
### Do Not
|
|
|
|
- ❌ Add configuration that requires editing multiple files
|
|
- ❌ Use deprecated Authelia configuration syntax
|
|
- ❌ Put secrets in environment variables (use _FILE pattern)
|
|
- ❌ Create services without proper health checks
|
|
- ❌ Bypass authentication (all services must auth via Authelia)
|
|
- ❌ Hardcode domains, IPs, or credentials
|
|
- ❌ Use `latest` tags for critical services (prefer versioned tags)
|
|
- ❌ Mix forward-auth and OIDC on same service
|
|
|
|
### Always
|
|
|
|
- ✅ Use file-based secrets
|
|
- ✅ Mount secrets read-only (`:ro`)
|
|
- ✅ Add educational comments to configs
|
|
- ✅ Test full deployment flow
|
|
- ✅ Keep `.env.example` in sync with actual usage
|
|
- ✅ Update documentation when changing behavior
|
|
- ✅ Use modern Authelia v4.38+ syntax
|
|
- ✅ Reference official docs/RFCs in comments
|
|
|
|
## Resources
|
|
|
|
### Official Documentation
|
|
- [lldap](https://github.com/lldap/lldap)
|
|
- [Authelia](https://www.authelia.com/)
|
|
- [Gitea](https://docs.gitea.io/)
|
|
- [JSPWiki](https://jspwiki-wiki.apache.org/)
|
|
- [Caddy](https://caddyserver.com/docs/)
|
|
|
|
### Protocol Specifications
|
|
- [LDAP - RFC 4511](https://tools.ietf.org/html/rfc4511)
|
|
- [OIDC Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
|
|
- [OAuth 2.0 - RFC 6749](https://tools.ietf.org/html/rfc6749)
|
|
- [TOTP - RFC 6238](https://tools.ietf.org/html/rfc6238)
|
|
|
|
### Helpful Articles
|
|
- [Understanding OIDC](https://connect2id.com/learn/openid-connect)
|
|
- [Forward-Auth Pattern](https://www.authelia.com/integration/proxies/fowarded-headers/)
|
|
- [Docker Secrets Best Practices](https://docs.docker.com/engine/swarm/secrets/)
|
|
|
|
## Project Goals
|
|
|
|
This project aims to:
|
|
1. **Demonstrate** modern authentication patterns (LDAP, OIDC, Forward-Auth)
|
|
2. **Educate** about SSO, 2FA, and centralized user management
|
|
3. **Provide** production-ready self-hosted alternative to cloud services
|
|
4. **Showcase** Docker Compose best practices
|
|
5. **Maintain** simplicity - one command deployment, zero manual config
|
|
|
|
When working on this project, prioritize these goals:
|
|
- Make it easier to deploy
|
|
- Make it more educational
|
|
- Make it more secure
|
|
- Make it well-documented
|
|
|
|
## Current State
|
|
|
|
**Production Ready**: Yes (with appropriate configuration)
|
|
**Deployment Model**: Remote server via SSH/rsync
|
|
**Supported Platforms**: Linux (Docker required)
|
|
**Active Development**: Yes
|
|
|
|
**Known Limitations**:
|
|
- SQLite databases (acceptable for <10k users)
|
|
- Filesystem notifier (should be SMTP for production)
|
|
- Single server deployment (no HA)
|
|
|
|
**Improvement Opportunities**:
|
|
- Add Redis for Authelia session storage
|
|
- Add PostgreSQL option for Gitea
|
|
- Add Prometheus monitoring
|
|
- Add automated backup scheduling
|
|
- Add user import scripts (CSV → lldap)
|
|
|
|
## Final Notes
|
|
|
|
This is a **teaching project** as much as a production tool. Code should be:
|
|
- **Readable** - clear variable names, educational comments
|
|
- **Reliable** - error handling, validation, health checks
|
|
- **Reproducible** - anyone should be able to deploy successfully
|
|
- **Referenced** - link to RFCs, docs, and explanations
|
|
|
|
When in doubt, prioritize clarity over cleverness, and security over convenience.
|
|
|
|
If you're working on this project as an AI assistant, remember:
|
|
- The user may not be an expert in authentication protocols
|
|
- Comments should explain "why" not just "what"
|
|
- Changes should work end-to-end (don't break deployment)
|
|
- Documentation is as important as code
|
|
|
|
Good luck! 🚀
|