Add comprehensive authentik documentation and improve role configuration
- Add authentik-deployment-guide.md: Complete step-by-step deployment guide - Add architecture-decisions.md: Document native DB vs containerized rationale - Add authentication-architecture.md: SSO strategy and integration patterns - Update deployment-guide.md: Integrate authentik deployment procedures - Update security-hardening.md: Add multi-layer security documentation - Update service-integration-guide.md: Add authentik integration examples - Update README.md: Professional project overview with architecture benefits - Update authentik role: Fix HTTP binding, add security configs, improve templates - Remove unused authentik task files: containers.yml, networking.yml Key improvements: * Document security benefits of native databases over containers * Document Unix socket IPC architecture advantages * Provide comprehensive troubleshooting and deployment procedures * Add forward auth integration patterns for services * Fix authentik HTTP binding from 127.0.0.1 to 0.0.0.0 * Add shared memory and IPC security configurations
This commit is contained in:
@@ -301,6 +301,499 @@ If you get permission denied errors:
|
||||
- Check service logs for connection errors
|
||||
- Verify group memberships after user changes
|
||||
|
||||
## Authentication Integration with Authentik
|
||||
|
||||
### Overview
|
||||
|
||||
All services in rick-infra should integrate with Authentik for centralized authentication and authorization. This section covers the authentication integration patterns available.
|
||||
|
||||
### Authentication Integration Patterns
|
||||
|
||||
#### Pattern 1: Forward Authentication (Recommended)
|
||||
|
||||
**Use Case**: HTTP services that don't need to handle authentication internally
|
||||
|
||||
**Benefits**:
|
||||
- No application code changes required
|
||||
- Consistent authentication across all services
|
||||
- Centralized session management
|
||||
- Service receives user identity via HTTP headers
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```yaml
|
||||
# Service role task to deploy Caddy configuration
|
||||
- name: Deploy service Caddy configuration with Authentik forward auth
|
||||
template:
|
||||
src: myservice.caddy.j2
|
||||
dest: "{{ caddy_sites_enabled_dir }}/myservice.caddy"
|
||||
owner: root
|
||||
group: "{{ caddy_user }}"
|
||||
mode: '0644'
|
||||
backup: true
|
||||
notify: reload caddy
|
||||
tags: [caddy, auth]
|
||||
```
|
||||
|
||||
```caddyfile
|
||||
# templates/myservice.caddy.j2
|
||||
{{ service_domain }} {
|
||||
# Forward authentication to Authentik
|
||||
forward_auth https://auth.jnss.me {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
|
||||
}
|
||||
|
||||
# Your service backend
|
||||
reverse_proxy {{ service_backend }}
|
||||
|
||||
# Optional: Restrict access by group
|
||||
@not_authorized {
|
||||
not header Remote-Groups "*{{ required_group }}*"
|
||||
}
|
||||
respond @not_authorized "Access denied: insufficient privileges" 403
|
||||
}
|
||||
```
|
||||
|
||||
**Service Code Example** (Python Flask):
|
||||
```python
|
||||
# Application receives authentication information via headers
|
||||
from flask import Flask, request
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
# Extract user information from headers (provided by Authentik)
|
||||
username = request.headers.get('Remote-User')
|
||||
user_name = request.headers.get('Remote-Name')
|
||||
user_email = request.headers.get('Remote-Email')
|
||||
user_groups = request.headers.get('Remote-Groups', '').split(',')
|
||||
|
||||
# Authorization based on groups
|
||||
if not username:
|
||||
return "Authentication required", 401
|
||||
|
||||
if 'service_users' not in user_groups:
|
||||
return "Access denied: insufficient privileges", 403
|
||||
|
||||
return render_template('dashboard.html',
|
||||
username=username,
|
||||
name=user_name,
|
||||
groups=user_groups)
|
||||
|
||||
@app.route('/admin')
|
||||
def admin():
|
||||
user_groups = request.headers.get('Remote-Groups', '').split(',')
|
||||
|
||||
if 'admins' not in user_groups:
|
||||
return "Admin access required", 403
|
||||
|
||||
return render_template('admin.html')
|
||||
```
|
||||
|
||||
#### Pattern 2: OAuth2/OIDC Integration
|
||||
|
||||
**Use Case**: Applications that can implement OAuth2 client functionality
|
||||
|
||||
**Benefits**:
|
||||
- Fine-grained scope control
|
||||
- API access tokens
|
||||
- Better integration with application user models
|
||||
- Support for mobile/SPA applications
|
||||
|
||||
**Service Configuration**:
|
||||
|
||||
```yaml
|
||||
# Service environment configuration
|
||||
oauth2_config:
|
||||
client_id: "{{ service_oauth_client_id }}"
|
||||
client_secret: "{{ vault_service_oauth_secret }}"
|
||||
discovery_url: "https://auth.jnss.me/application/o/{{ service_slug }}/.well-known/openid_configuration"
|
||||
scopes: "openid email profile groups"
|
||||
redirect_uri: "https://{{ service_domain }}/oauth/callback"
|
||||
```
|
||||
|
||||
```python
|
||||
# OAuth2 integration example (Python)
|
||||
from authlib.integrations.flask_client import OAuth
|
||||
|
||||
oauth = OAuth(app)
|
||||
oauth.register(
|
||||
'authentik',
|
||||
client_id=app.config['OAUTH_CLIENT_ID'],
|
||||
client_secret=app.config['OAUTH_CLIENT_SECRET'],
|
||||
server_metadata_url=app.config['OAUTH_DISCOVERY_URL'],
|
||||
client_kwargs={
|
||||
'scope': 'openid email profile groups'
|
||||
}
|
||||
)
|
||||
|
||||
@app.route('/login')
|
||||
def login():
|
||||
redirect_uri = url_for('oauth_callback', _external=True)
|
||||
return oauth.authentik.authorize_redirect(redirect_uri)
|
||||
|
||||
@app.route('/oauth/callback')
|
||||
def oauth_callback():
|
||||
token = oauth.authentik.authorize_access_token()
|
||||
user_info = oauth.authentik.parse_id_token(token)
|
||||
|
||||
# Store user information in session
|
||||
session['user'] = {
|
||||
'id': user_info['sub'],
|
||||
'username': user_info['preferred_username'],
|
||||
'email': user_info['email'],
|
||||
'name': user_info['name'],
|
||||
'groups': user_info.get('groups', [])
|
||||
}
|
||||
|
||||
return redirect('/dashboard')
|
||||
```
|
||||
|
||||
#### Pattern 3: API-Only Authentication
|
||||
|
||||
**Use Case**: REST APIs, microservices, machine-to-machine communication
|
||||
|
||||
**Implementation**:
|
||||
|
||||
```python
|
||||
# API service with token validation
|
||||
import requests
|
||||
from flask import Flask, request, jsonify
|
||||
|
||||
def validate_token(token):
|
||||
"""Validate Bearer token with Authentik introspection endpoint"""
|
||||
try:
|
||||
response = requests.post(
|
||||
'https://auth.jnss.me/application/o/introspect/',
|
||||
headers={
|
||||
'Authorization': f'Bearer {app.config["API_CLIENT_TOKEN"]}'
|
||||
},
|
||||
data={'token': token},
|
||||
timeout=5
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
return None
|
||||
except Exception as e:
|
||||
app.logger.error(f"Token validation error: {e}")
|
||||
return None
|
||||
|
||||
@app.route('/api/data')
|
||||
def api_data():
|
||||
auth_header = request.headers.get('Authorization')
|
||||
|
||||
if not auth_header or not auth_header.startswith('Bearer '):
|
||||
return jsonify({'error': 'Missing or invalid Authorization header'}), 401
|
||||
|
||||
token = auth_header[7:] # Remove 'Bearer ' prefix
|
||||
token_info = validate_token(token)
|
||||
|
||||
if not token_info or not token_info.get('active'):
|
||||
return jsonify({'error': 'Invalid or expired token'}), 401
|
||||
|
||||
# Extract user information from token
|
||||
username = token_info.get('username')
|
||||
scope = token_info.get('scope', '').split()
|
||||
|
||||
# Check required scope
|
||||
if 'api:read' not in scope:
|
||||
return jsonify({'error': 'Insufficient permissions'}), 403
|
||||
|
||||
return jsonify({
|
||||
'message': f'Hello {username}',
|
||||
'data': 'Your API response data here'
|
||||
})
|
||||
```
|
||||
|
||||
### Authentik Provider Configuration
|
||||
|
||||
For each service integration, configure the appropriate provider in Authentik:
|
||||
|
||||
#### Forward Auth Provider (Pattern 1)
|
||||
|
||||
```yaml
|
||||
# Authentik provider configuration (via admin interface)
|
||||
provider_config:
|
||||
name: "{{ service_name }} Forward Auth"
|
||||
type: "Proxy Provider"
|
||||
authorization_flow: "default-provider-authorization-implicit-consent"
|
||||
external_host: "https://{{ service_domain }}"
|
||||
internal_host: "http://localhost:{{ service_port }}"
|
||||
skip_path_regex: "^/(health|metrics|static).*"
|
||||
```
|
||||
|
||||
#### OAuth2 Provider (Pattern 2)
|
||||
|
||||
```yaml
|
||||
# Authentik OAuth2 provider configuration
|
||||
oauth_provider_config:
|
||||
name: "{{ service_name }} OAuth2"
|
||||
type: "OAuth2/OpenID Provider"
|
||||
authorization_flow: "default-provider-authorization-explicit-consent"
|
||||
client_type: "confidential"
|
||||
client_id: "{{ service_oauth_client_id }}"
|
||||
redirect_uris:
|
||||
- "https://{{ service_domain }}/oauth/callback"
|
||||
post_logout_redirect_uris:
|
||||
- "https://{{ service_domain }}/"
|
||||
```
|
||||
|
||||
#### API Provider (Pattern 3)
|
||||
|
||||
```yaml
|
||||
# Authentik API provider configuration
|
||||
api_provider_config:
|
||||
name: "{{ service_name }} API"
|
||||
type: "OAuth2/OpenID Provider"
|
||||
authorization_flow: "default-provider-authorization-implicit-consent"
|
||||
client_type: "confidential"
|
||||
client_id: "{{ service_api_client_id }}"
|
||||
include_claims_in_id_token: true
|
||||
issuer_mode: "per_provider"
|
||||
```
|
||||
|
||||
### Group-Based Authorization
|
||||
|
||||
#### Service-Specific Groups
|
||||
|
||||
```yaml
|
||||
# Create service-specific groups in Authentik
|
||||
service_groups:
|
||||
- name: "{{ service_name }}_users"
|
||||
description: "Users who can access {{ service_name }}"
|
||||
is_superuser: false
|
||||
|
||||
- name: "{{ service_name }}_admins"
|
||||
description: "Administrators for {{ service_name }}"
|
||||
is_superuser: false
|
||||
parent: "{{ service_name }}_users"
|
||||
|
||||
- name: "{{ service_name }}_readonly"
|
||||
description: "Read-only access to {{ service_name }}"
|
||||
is_superuser: false
|
||||
parent: "{{ service_name }}_users"
|
||||
```
|
||||
|
||||
#### Policy-Based Access Control
|
||||
|
||||
```yaml
|
||||
# Authentik policies for service access
|
||||
service_policies:
|
||||
- name: "{{ service_name }} Group Access"
|
||||
policy_type: "Group Membership Policy"
|
||||
groups: ["{{ service_name }}_users"]
|
||||
|
||||
- name: "{{ service_name }} Business Hours"
|
||||
policy_type: "Time-based Policy"
|
||||
parameters:
|
||||
start_time: "08:00"
|
||||
end_time: "18:00"
|
||||
days: ["monday", "tuesday", "wednesday", "thursday", "friday"]
|
||||
|
||||
- name: "{{ service_name }} IP Restriction"
|
||||
policy_type: "Source IP Policy"
|
||||
parameters:
|
||||
cidr: "10.0.0.0/8"
|
||||
```
|
||||
|
||||
### Service Role Template with Authentication
|
||||
|
||||
Here's a complete service role template that includes authentication integration:
|
||||
|
||||
```yaml
|
||||
# roles/myservice/defaults/main.yml
|
||||
---
|
||||
# Service configuration
|
||||
service_name: "myservice"
|
||||
service_domain: "myservice.jnss.me"
|
||||
service_port: 8080
|
||||
service_backend: "localhost:{{ service_port }}"
|
||||
|
||||
# Authentication configuration
|
||||
auth_enabled: true
|
||||
auth_pattern: "forward_auth" # forward_auth, oauth2, api_only
|
||||
required_group: "myservice_users"
|
||||
|
||||
# OAuth2 configuration (if auth_pattern is oauth2)
|
||||
oauth_client_id: "{{ service_name }}-oauth-client"
|
||||
oauth_client_secret: "{{ vault_myservice_oauth_secret }}"
|
||||
|
||||
# Dependencies
|
||||
postgresql_db_name: "{{ service_name }}"
|
||||
valkey_db_number: 2
|
||||
```
|
||||
|
||||
```yaml
|
||||
# roles/myservice/tasks/main.yml
|
||||
---
|
||||
- name: Create service user and setup
|
||||
include_tasks: user.yml
|
||||
tags: [user, setup]
|
||||
|
||||
- name: Setup database access
|
||||
include_tasks: database.yml
|
||||
tags: [database, setup]
|
||||
|
||||
- name: Setup cache access
|
||||
include_tasks: cache.yml
|
||||
tags: [cache, setup]
|
||||
|
||||
- name: Deploy service configuration
|
||||
template:
|
||||
src: myservice.env.j2
|
||||
dest: "{{ service_home }}/.env"
|
||||
owner: "{{ service_user }}"
|
||||
group: "{{ service_group }}"
|
||||
mode: '0600'
|
||||
tags: [config]
|
||||
|
||||
- name: Deploy container configuration
|
||||
template:
|
||||
src: "{{ item.src }}"
|
||||
dest: "{{ service_quadlet_dir }}/{{ item.dest }}"
|
||||
owner: "{{ service_user }}"
|
||||
group: "{{ service_group }}"
|
||||
mode: '0644'
|
||||
loop:
|
||||
- { src: 'myservice.pod', dest: 'myservice.pod' }
|
||||
- { src: 'myservice.container', dest: 'myservice.container' }
|
||||
become: true
|
||||
become_user: "{{ service_user }}"
|
||||
notify:
|
||||
- reload systemd user
|
||||
- restart myservice
|
||||
tags: [containers]
|
||||
|
||||
- name: Deploy Caddy configuration with authentication
|
||||
template:
|
||||
src: myservice.caddy.j2
|
||||
dest: "{{ caddy_sites_enabled_dir }}/{{ service_name }}.caddy"
|
||||
owner: root
|
||||
group: "{{ caddy_user }}"
|
||||
mode: '0644'
|
||||
backup: true
|
||||
notify: reload caddy
|
||||
tags: [caddy, auth]
|
||||
when: auth_enabled
|
||||
|
||||
- name: Start and enable service
|
||||
systemd:
|
||||
name: "{{ service_name }}"
|
||||
enabled: true
|
||||
state: started
|
||||
scope: user
|
||||
daemon_reload: true
|
||||
become: true
|
||||
become_user: "{{ service_user }}"
|
||||
tags: [service]
|
||||
```
|
||||
|
||||
```caddyfile
|
||||
# roles/myservice/templates/myservice.caddy.j2
|
||||
{{ service_domain }} {
|
||||
{% if auth_enabled and auth_pattern == 'forward_auth' %}
|
||||
# Forward authentication to Authentik
|
||||
forward_auth https://auth.jnss.me {
|
||||
uri /outpost.goauthentik.io/auth/caddy
|
||||
copy_headers Remote-User Remote-Name Remote-Email Remote-Groups
|
||||
}
|
||||
|
||||
{% if required_group %}
|
||||
# Restrict access to specific group
|
||||
@not_authorized {
|
||||
not header Remote-Groups "*{{ required_group }}*"
|
||||
}
|
||||
respond @not_authorized "Access denied: {{ required_group }} group required" 403
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
# Service backend
|
||||
reverse_proxy {{ service_backend }}
|
||||
|
||||
# Health check endpoint (no auth required)
|
||||
handle /health {
|
||||
reverse_proxy {{ service_backend }}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
|
||||
#### Authentication Integration Tests
|
||||
|
||||
```yaml
|
||||
# Service authentication tests
|
||||
authentication_tests:
|
||||
- name: "Test unauthenticated access denied"
|
||||
uri: "https://{{ service_domain }}/"
|
||||
method: "GET"
|
||||
expected_status: [302, 401] # Redirect to login or unauthorized
|
||||
|
||||
- name: "Test authenticated access allowed"
|
||||
uri: "https://{{ service_domain }}/"
|
||||
method: "GET"
|
||||
headers:
|
||||
Cookie: "authentik_session={{ valid_session_cookie }}"
|
||||
expected_status: [200]
|
||||
|
||||
- name: "Test group authorization"
|
||||
uri: "https://{{ service_domain }}/admin"
|
||||
method: "GET"
|
||||
headers:
|
||||
Cookie: "authentik_session={{ admin_session_cookie }}"
|
||||
expected_status: [200]
|
||||
|
||||
- name: "Test insufficient privileges"
|
||||
uri: "https://{{ service_domain }}/admin"
|
||||
method: "GET"
|
||||
headers:
|
||||
Cookie: "authentik_session={{ user_session_cookie }}"
|
||||
expected_status: [403]
|
||||
```
|
||||
|
||||
#### Automated Testing Script
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# test-service-auth.sh
|
||||
|
||||
SERVICE_DOMAIN="myservice.jnss.me"
|
||||
|
||||
echo "Testing service authentication for $SERVICE_DOMAIN"
|
||||
|
||||
# Test 1: Unauthenticated access should redirect or deny
|
||||
echo "Test 1: Unauthenticated access"
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://$SERVICE_DOMAIN/")
|
||||
if [[ "$RESPONSE" == "302" || "$RESPONSE" == "401" ]]; then
|
||||
echo "✓ PASS: Unauthenticated access properly denied ($RESPONSE)"
|
||||
else
|
||||
echo "✗ FAIL: Unauthenticated access allowed ($RESPONSE)"
|
||||
fi
|
||||
|
||||
# Test 2: Health endpoint should be accessible
|
||||
echo "Test 2: Health endpoint access"
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://$SERVICE_DOMAIN/health")
|
||||
if [[ "$RESPONSE" == "200" ]]; then
|
||||
echo "✓ PASS: Health endpoint accessible"
|
||||
else
|
||||
echo "✗ FAIL: Health endpoint not accessible ($RESPONSE)"
|
||||
fi
|
||||
|
||||
# Test 3: Authentik forward auth endpoint exists
|
||||
echo "Test 3: Authentik forward auth endpoint"
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" "https://auth.jnss.me/outpost.goauthentik.io/auth/caddy")
|
||||
if [[ "$RESPONSE" == "401" ]]; then
|
||||
echo "✓ PASS: Forward auth endpoint responding"
|
||||
else
|
||||
echo "✗ FAIL: Forward auth endpoint not responding correctly ($RESPONSE)"
|
||||
fi
|
||||
|
||||
echo "Authentication tests completed"
|
||||
```
|
||||
|
||||
## Example Integration
|
||||
|
||||
See the `authentik` role for a complete example of this pattern:
|
||||
@@ -309,4 +802,39 @@ See the `authentik` role for a complete example of this pattern:
|
||||
- **Tasks**: `roles/authentik/tasks/`
|
||||
- **Documentation**: `roles/authentik/README.md`
|
||||
|
||||
This provides a working reference implementation for Unix socket integration.
|
||||
This provides a working reference implementation for Unix socket integration.
|
||||
|
||||
## Authentication Integration Examples
|
||||
|
||||
For practical authentication integration examples, see:
|
||||
|
||||
- **[Authentication Architecture](authentication-architecture.md)** - Complete authentication patterns and examples
|
||||
- **[Authentik Deployment Guide](authentik-deployment-guide.md)** - Authentik-specific configuration
|
||||
- **[Architecture Decisions](architecture-decisions.md)** - Authentication model rationale
|
||||
|
||||
## Quick Start Checklist
|
||||
|
||||
When integrating a new service with rick-infra:
|
||||
|
||||
### Infrastructure Integration
|
||||
- [ ] Create dedicated system user for the service
|
||||
- [ ] Add user to `postgres` and `valkey` groups for database access
|
||||
- [ ] Configure Unix socket connections in service environment
|
||||
- [ ] Set up Quadlet container configuration with proper user/group settings
|
||||
- [ ] Test database and cache connectivity
|
||||
|
||||
### Authentication Integration
|
||||
- [ ] Choose authentication pattern (forward auth recommended for most services)
|
||||
- [ ] Create Caddy configuration with Authentik forward auth
|
||||
- [ ] Create Authentik provider and application configuration
|
||||
- [ ] Set up service-specific groups and policies in Authentik
|
||||
- [ ] Test authentication flow and authorization
|
||||
|
||||
### Deployment Integration
|
||||
- [ ] Add service role dependencies in `meta/main.yml`
|
||||
- [ ] Configure service in `site.yml` with appropriate tags
|
||||
- [ ] Set up vault variables for secrets
|
||||
- [ ] Deploy and verify service functionality
|
||||
- [ ] Add monitoring and backup procedures
|
||||
|
||||
This comprehensive integration approach ensures all services benefit from the security, performance, and operational advantages of the rick-infra architecture.
|
||||
Reference in New Issue
Block a user