This commit captures the infrastructure state immediately following the "Post-Tyranny Tech" workshop on January 23rd, 2026. Infrastructure Status: - 13 client servers deployed (white, valk, zwaan, specht, das, uil, vos, haas, wolf, ree, mees, mus, mol, kikker) - Services: Authentik SSO, Nextcloud, Collabora Office, Traefik - Private network architecture with edge NAT gateway - OIDC integration between Authentik and Nextcloud - Automated recovery flows and invitation system - Container update monitoring with Diun - Uptime monitoring with Uptime Kuma Changes include: - Multiple new client host configurations - Network architecture improvements (private IPs + NAT) - DNS management automation - Container update notifications - Email configuration via Mailgun - SSH key generation for all clients - Encrypted secrets for all deployments - Health check and diagnostic scripts Known Issues to Address: - Nextcloud version pinned to v30 (should use 'latest' or v32) - Zitadel references in templates (migrated to Authentik but templates not updated) - Traefik dynamic config has obsolete static routes 🤖 Generated with Claude Code (https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
156 lines
6.6 KiB
Bash
Executable file
156 lines
6.6 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
#
|
|
# Configure OIDC for a single client
|
|
#
|
|
# Usage: ./scripts/configure-oidc.sh <client_name>
|
|
#
|
|
# This script:
|
|
# 1. Creates OIDC provider in Authentik
|
|
# 2. Installs user_oidc app in Nextcloud
|
|
# 3. Configures OIDC connection
|
|
# 4. Enables multiple user backends
|
|
|
|
set -euo pipefail
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
# Script directory
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
|
|
|
|
# Check arguments
|
|
if [ $# -ne 1 ]; then
|
|
echo -e "${RED}Error: Client name required${NC}"
|
|
echo "Usage: $0 <client_name>"
|
|
exit 1
|
|
fi
|
|
|
|
CLIENT_NAME="$1"
|
|
|
|
# Check environment variables
|
|
if [ -z "${SOPS_AGE_KEY_FILE:-}" ]; then
|
|
export SOPS_AGE_KEY_FILE="$PROJECT_ROOT/keys/age-key.txt"
|
|
fi
|
|
|
|
if [ -z "${HCLOUD_TOKEN:-}" ]; then
|
|
echo -e "${RED}Error: HCLOUD_TOKEN not set${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${BLUE}Configuring OIDC for ${CLIENT_NAME}...${NC}"
|
|
|
|
cd "$PROJECT_ROOT"
|
|
|
|
# Step 1: Get credentials from secrets
|
|
echo "Getting credentials from secrets..."
|
|
TOKEN=$(sops -d "secrets/clients/${CLIENT_NAME}.sops.yaml" | grep authentik_bootstrap_token | awk '{print $2}')
|
|
|
|
if [ -z "$TOKEN" ]; then
|
|
echo -e "${RED}Error: Could not get Authentik token${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Step 2: Create OIDC provider in Authentik
|
|
echo "Creating OIDC provider in Authentik..."
|
|
|
|
# Create Python script
|
|
cat > /tmp/create_oidc_${CLIENT_NAME}.py << EOFPYTHON
|
|
import sys, json, urllib.request
|
|
base_url, token = "http://localhost:9000", "${TOKEN}"
|
|
def req(p, m='GET', d=None):
|
|
r = urllib.request.Request(f"{base_url}{p}", json.dumps(d).encode() if d else None, {'Authorization': f'Bearer {token}', 'Content-Type': 'application/json'}, method=m)
|
|
try:
|
|
with urllib.request.urlopen(r, timeout=30) as resp: return resp.status, json.loads(resp.read())
|
|
except urllib.error.HTTPError as e: return e.code, json.loads(e.read()) if e.headers.get('Content-Type', '').startswith('application/json') else {'error': e.read().decode()}
|
|
s, d = req('/api/v3/flows/instances/')
|
|
auth_flow = next((f['pk'] for f in d.get('results', []) if f.get('slug') == 'default-authorization-flow' or f.get('designation') == 'authorization'), None)
|
|
inval_flow = next((f['pk'] for f in d.get('results', []) if f.get('slug') == 'default-invalidation-flow' or f.get('designation') == 'invalidation'), None)
|
|
s, d = req('/api/v3/crypto/certificatekeypairs/')
|
|
key = d.get('results', [{}])[0].get('pk') if d.get('results') else None
|
|
if not auth_flow or not key: print(json.dumps({'error': 'Config missing', 'auth_flow': auth_flow, 'key': key}), file=sys.stderr); sys.exit(1)
|
|
s, prov = req('/api/v3/providers/oauth2/', 'POST', {'name': 'Nextcloud', 'authorization_flow': auth_flow, 'invalidation_flow': inval_flow, 'client_type': 'confidential', 'redirect_uris': [{'matching_mode': 'strict', 'url': 'https://nextcloud.${CLIENT_NAME}.vrije.cloud/apps/user_oidc/code'}], 'signing_key': key, 'sub_mode': 'hashed_user_id', 'include_claims_in_id_token': True})
|
|
if s != 201: print(json.dumps({'error': 'Provider failed', 'status': s, 'details': prov}), file=sys.stderr); sys.exit(1)
|
|
s, app = req('/api/v3/core/applications/', 'POST', {'name': 'Nextcloud', 'slug': 'nextcloud', 'provider': prov['pk'], 'meta_launch_url': 'https://nextcloud.${CLIENT_NAME}.vrije.cloud'})
|
|
if s != 201: print(json.dumps({'error': 'App failed', 'status': s, 'details': app}), file=sys.stderr); sys.exit(1)
|
|
print(json.dumps({'success': True, 'provider_id': prov['pk'], 'application_id': app['pk'], 'client_id': prov['client_id'], 'client_secret': prov['client_secret'], 'discovery_uri': f"https://auth.${CLIENT_NAME}.vrije.cloud/application/o/nextcloud/.well-known/openid-configuration", 'issuer': f"https://auth.${CLIENT_NAME}.vrije.cloud/application/o/nextcloud/"}))
|
|
EOFPYTHON
|
|
|
|
# Copy script to server and execute
|
|
cd ansible
|
|
env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
|
ansible "${CLIENT_NAME}" \
|
|
-i hcloud.yml \
|
|
-m copy \
|
|
-a "src=/tmp/create_oidc_${CLIENT_NAME}.py dest=/tmp/create_oidc.py mode=0755" \
|
|
--private-key "../keys/ssh/${CLIENT_NAME}" > /dev/null 2>&1
|
|
|
|
# Execute the script
|
|
OIDC_RESULT=$(env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
|
ansible "${CLIENT_NAME}" \
|
|
-i hcloud.yml \
|
|
-m shell \
|
|
-a "docker exec -i authentik-server python3 < /tmp/create_oidc.py" \
|
|
--private-key "../keys/ssh/${CLIENT_NAME}" 2>/dev/null | grep -A1 "CHANGED" | tail -1)
|
|
|
|
if [ -z "$OIDC_RESULT" ]; then
|
|
echo -e "${RED}Error: Failed to create OIDC provider${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
# Parse credentials
|
|
CLIENT_ID=$(echo "$OIDC_RESULT" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['client_id'])")
|
|
CLIENT_SECRET=$(echo "$OIDC_RESULT" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['client_secret'])")
|
|
DISCOVERY_URI=$(echo "$OIDC_RESULT" | python3 -c "import sys, json; d=json.load(sys.stdin); print(d['discovery_uri'])")
|
|
|
|
if [ -z "$CLIENT_ID" ] || [ -z "$CLIENT_SECRET" ] || [ -z "$DISCOVERY_URI" ]; then
|
|
echo -e "${RED}Error: Failed to parse OIDC credentials${NC}"
|
|
exit 1
|
|
fi
|
|
|
|
echo -e "${GREEN}✓ OIDC provider created${NC}"
|
|
|
|
# Step 3: Install user_oidc app in Nextcloud
|
|
echo "Installing user_oidc app..."
|
|
|
|
env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
|
ansible "${CLIENT_NAME}" \
|
|
-i hcloud.yml \
|
|
-m shell \
|
|
-a "docker exec -u www-data nextcloud php occ app:install user_oidc" \
|
|
--private-key "../keys/ssh/${CLIENT_NAME}" > /dev/null 2>&1 || true
|
|
|
|
echo -e "${GREEN}✓ user_oidc app installed${NC}"
|
|
|
|
# Step 4: Configure OIDC provider in Nextcloud
|
|
echo "Configuring OIDC provider..."
|
|
|
|
env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
|
ansible "${CLIENT_NAME}" \
|
|
-i hcloud.yml \
|
|
-m shell \
|
|
-a "docker exec -u www-data nextcloud php occ user_oidc:provider --clientid=\"${CLIENT_ID}\" --clientsecret=\"${CLIENT_SECRET}\" --discoveryuri=\"${DISCOVERY_URI}\" \"Authentik\"" \
|
|
--private-key "../keys/ssh/${CLIENT_NAME}" > /dev/null 2>&1
|
|
|
|
echo -e "${GREEN}✓ OIDC provider configured${NC}"
|
|
|
|
# Step 5: Configure OIDC settings
|
|
echo "Configuring OIDC settings..."
|
|
|
|
env HCLOUD_TOKEN="$HCLOUD_TOKEN" \
|
|
ansible "${CLIENT_NAME}" \
|
|
-i hcloud.yml \
|
|
-m shell \
|
|
-a "docker exec -u www-data nextcloud php occ config:app:set user_oidc allow_multiple_user_backends --value=1 && docker exec -u www-data nextcloud php occ config:app:set user_oidc auto_provision --value=1 && docker exec -u www-data nextcloud php occ config:app:set user_oidc single_logout --value=0" \
|
|
--private-key "../keys/ssh/${CLIENT_NAME}" > /dev/null 2>&1
|
|
|
|
echo -e "${GREEN}✓ OIDC settings configured${NC}"
|
|
|
|
# Cleanup
|
|
rm -f /tmp/create_oidc_${CLIENT_NAME}.py
|
|
|
|
echo -e "${GREEN}✓ OIDC configuration complete for ${CLIENT_NAME}${NC}"
|