Post-Tyranny-Tech-Infrastru.../scripts/configure-oidc.sh
Pieter b6c9fa666d chore: Post-workshop state - January 23rd, 2026
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>
2026-01-23 20:36:31 +01:00

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}"