#!/usr/bin/env bash set -euo pipefail # ============================================================ # create-auth-app.sh — Crea una aplicación protegida por # Authentik (Proxy Provider + Application + Outpost) vía API # ============================================================ # # Uso: # ./create-auth-app.sh [nombre] [slug] # # Ejemplos: # ./create-auth-app.sh crowdsec.sherlockhomeless.net "CrowdSec" crowdsec # ./create-auth-app.sh mi-app.midominio.com # # Requisitos: # - Contenedor ths-authentik-server corriendo y healthy # - jq instalado (sudo apt install jq) # ============================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" # ----------------------------------------------------------- # Cargar configuración # ----------------------------------------------------------- AK_HOST_DEFAULT="https://auth.sherlockhomeless.net" AK_CONTAINER="ths-authentik-server" if [ -f "${ENV_FILE}" ]; then set -a; source "${ENV_FILE}"; set +a fi AK_HOST="${AUTHENTIK_DOMAIN:-$AK_HOST_DEFAULT}" AK_HOST="https://${AK_HOST#https://}" # normalizar # ----------------------------------------------------------- # Parámetros # ----------------------------------------------------------- DOMAIN="${1:-}" APP_NAME="${2:-}" APP_SLUG="${3:-}" if [ -z "$DOMAIN" ]; then echo "Uso: $0 [nombre] [slug]" echo "" echo "Ejemplos:" echo " $0 crowdsec.sherlockhomeless.net CrowdSec crowdsec" echo " $0 mi-app.midominio.com" echo "" echo "Si no se especifica nombre/slug, se derivan del dominio." exit 1 fi # Derivar nombre y slug del dominio si no se especificaron if [ -z "$APP_NAME" ]; then # Extraer subdominio: mi-app.midominio.com -> Mi App SUB=$(echo "$DOMAIN" | cut -d. -f1 | sed 's/-/ /g' | sed 's/\b\(.\)/\u\1/g') APP_NAME="$SUB" fi if [ -z "$APP_SLUG" ]; then APP_SLUG=$(echo "$DOMAIN" | cut -d. -f1 | tr '[:upper:]' '[:lower:]') fi EXTERNAL_HOST="https://${DOMAIN}" echo "=========================================" echo " Authentik — Crear Application" echo "=========================================" echo " Dominio: ${DOMAIN}" echo " External host: ${EXTERNAL_HOST}" echo " Nombre app: ${APP_NAME}" echo " Slug: ${APP_SLUG}" echo "=========================================" echo "" # ----------------------------------------------------------- # Verificar dependencias # ----------------------------------------------------------- if ! command -v jq &>/dev/null; then echo "ERROR: jq no está instalado. Instálalo con: sudo apt install jq" exit 1 fi if ! docker ps --format '{{.Names}}' | grep -q "^${AK_CONTAINER}$"; then echo "ERROR: El contenedor '${AK_CONTAINER}' no está corriendo." exit 1 fi # ----------------------------------------------------------- # 1. Generar token temporal de API # ----------------------------------------------------------- echo "[1/6] Generando token temporal de API..." TOKEN_SCRIPT=" from authentik.core.models import Token, User user = User.objects.filter(username='akadmin').first() if not user: print('ERROR:NO_USER') else: token, _ = Token.objects.update_or_create( user=user, identifier='setup-script', defaults={'intent': 'api', 'expiring': True, 'expires': None, 'description': 'Token temporal para create-auth-app.sh'} ) print(token.key) " AK_TOKEN=$(docker exec "$AK_CONTAINER" bash -c "echo \"$TOKEN_SCRIPT\" | ak shell 2>/dev/null" | grep -v '^###' | grep -v '^$' | grep -v 'objects imported' | tail -1) if [ -z "$AK_TOKEN" ] || [[ "$AK_TOKEN" == ERROR:* ]]; then echo "ERROR: No se pudo generar el token de API. ¿Existe el usuario akadmin?" echo "Respuesta: $AK_TOKEN" exit 1 fi echo " Token obtenido correctamente." # ----------------------------------------------------------- # 2. Obtener el authorization flow # ----------------------------------------------------------- echo "[2/6] Buscando authorization flow (implicit-consent)..." FLOW_JSON=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/flows/instances/?slug=default-provider-authorization-implicit-consent" 2>/dev/null) FLOW_UUID=$(echo "$FLOW_JSON" | jq -r '.results[0].pk // empty') if [ -z "$FLOW_UUID" ]; then echo "ERROR: No se encontró el flow 'default-provider-authorization-implicit-consent'." exit 1 fi echo " Flow UUID: ${FLOW_UUID}" # Buscar un provider existente para copiar sus property_mappings e invalidation_flow EXISTING_PROVIDER=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/providers/proxy/?page_size=1" 2>/dev/null) INVAL_FLOW=$(echo "$EXISTING_PROVIDER" | jq -r '.results[0].invalidation_flow // empty') PROPERTY_MAPPINGS=$(echo "$EXISTING_PROVIDER" | jq -c '.results[0].property_mappings // []') # ----------------------------------------------------------- # 3. Verificar si ya existe un provider para este dominio # ----------------------------------------------------------- echo "[3/6] Verificando si ya existe un provider para ${EXTERNAL_HOST}..." # Listar todos los providers y filtrar por external_host exacto (el API no filtra por este campo) EXISTING_PK=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/providers/proxy/?page_size=100" 2>/dev/null | jq -r --arg HOST "${EXTERNAL_HOST}" '.results[] | select(.external_host == $HOST) | .pk' 2>/dev/null) if [ -n "$EXISTING_PK" ]; then echo " Ya existe un provider (pk=${EXISTING_PK}). Saltando creación." PROVIDER_PK="$EXISTING_PK" else # Crear provider PROVIDER_JSON=$(curl -sk -X POST "${AK_HOST}/api/v3/providers/proxy/" \ -H "Authorization: Bearer $AK_TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"Provider for ${APP_NAME}\", \"authentication_flow\": null, \"authorization_flow\": \"${FLOW_UUID}\", \"invalidation_flow\": \"${INVAL_FLOW}\", \"property_mappings\": ${PROPERTY_MAPPINGS}, \"external_host\": \"${EXTERNAL_HOST}\", \"internal_host\": \"\", \"internal_host_ssl_validation\": true, \"mode\": \"forward_single\", \"intercept_header_auth\": true, \"cookie_domain\": \"\", \"access_token_validity\": \"hours=1\", \"refresh_token_validity\": \"days=30\" }" 2>/dev/null) PROVIDER_PK=$(echo "$PROVIDER_JSON" | jq -r '.pk // empty') if [ -z "$PROVIDER_PK" ]; then echo "ERROR: No se pudo crear el provider." echo "Respuesta: $PROVIDER_JSON" exit 1 fi echo " Provider creado: pk=${PROVIDER_PK}" fi # ----------------------------------------------------------- # 4. Verificar si ya existe una aplicación con este slug # ----------------------------------------------------------- echo "[4/6] Verificando si ya existe la aplicación '${APP_SLUG}'..." EXISTING_APP=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/core/applications/?slug=${APP_SLUG}" 2>/dev/null | jq -r '.results[0].pk // empty') if [ -n "$EXISTING_APP" ]; then echo " Ya existe la aplicación (pk=${EXISTING_APP}). Saltando creación." APP_PK="$EXISTING_APP" else # Crear aplicación APP_JSON=$(curl -sk -X POST "${AK_HOST}/api/v3/core/applications/" \ -H "Authorization: Bearer $AK_TOKEN" \ -H "Content-Type: application/json" \ -d "{ \"name\": \"${APP_NAME}\", \"slug\": \"${APP_SLUG}\", \"provider\": ${PROVIDER_PK}, \"policy_engine_mode\": \"any\", \"meta_launch_url\": \"${EXTERNAL_HOST}\" }" 2>/dev/null) APP_PK=$(echo "$APP_JSON" | jq -r '.pk // empty') if [ -z "$APP_PK" ]; then echo "ERROR: No se pudo crear la aplicación." echo "Respuesta: $APP_JSON" exit 1 fi echo " Aplicación creada: pk=${APP_PK}" fi # ----------------------------------------------------------- # 5. Copiar policy binding de una app existente # ----------------------------------------------------------- echo "[5/6] Aplicando policy binding..." REFERENCE_APP_POLICY=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/policies/bindings/?target=${APP_PK}" 2>/dev/null | jq -r '.results[0].policy // empty') if [ -n "$REFERENCE_APP_POLICY" ]; then echo " La app ya tiene un binding. Saltando." else # Tomar la policy del primer binding de cualquier app existente SAMPLE_APP=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/core/applications/?page_size=5" 2>/dev/null | jq -r '.results[] | select(.slug != "'"${APP_SLUG}"'") | .pk' | head -1) POLICY_PK=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/policies/bindings/?target=${SAMPLE_APP}" 2>/dev/null | jq -r '.results[0].policy // empty') if [ -n "$POLICY_PK" ]; then curl -sk -X POST "${AK_HOST}/api/v3/policies/bindings/" \ -H "Authorization: Bearer $AK_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"policy\": \"${POLICY_PK}\", \"target\": \"${APP_PK}\", \"enabled\": true, \"order\": 0}" 2>/dev/null > /dev/null echo " Policy binding creado (policy=${POLICY_PK})." else echo " WARN: No se encontró una policy de referencia. Añade el binding manualmente en Authentik." fi fi # ----------------------------------------------------------- # 6. Obtener el Embedded Outpost # ----------------------------------------------------------- echo "[6/7] Buscando el Embedded Outpost..." OUTPOST_JSON=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/outposts/instances/?name=authentik%20Embedded%20Outpost" 2>/dev/null) OUTPOST_UUID=$(echo "$OUTPOST_JSON" | jq -r '.results[0].pk // empty') if [ -z "$OUTPOST_UUID" ]; then echo "ERROR: No se encontró el Embedded Outpost." exit 1 fi echo " Outpost UUID: ${OUTPOST_UUID}" # ----------------------------------------------------------- # 6. Añadir provider al outpost # ----------------------------------------------------------- echo "[7/7] Añadiendo provider al outpost..." CURRENT_PROVIDERS=$(curl -sk -H "Authorization: Bearer $AK_TOKEN" \ "${AK_HOST}/api/v3/outposts/instances/${OUTPOST_UUID}/" 2>/dev/null | jq -r '.providers | join(",")') # Verificar si el provider ya está en la lista if echo "$CURRENT_PROVIDERS" | grep -qw "${PROVIDER_PK}"; then echo " El provider ya está asociado al outpost." else NEW_PROVIDERS="${CURRENT_PROVIDERS},${PROVIDER_PK}" # Convertir a array JSON PROVIDER_ARRAY=$(echo "$NEW_PROVIDERS" | tr ',' '\n' | jq -R 'tonumber' | jq -s '.') curl -sk -X PATCH "${AK_HOST}/api/v3/outposts/instances/${OUTPOST_UUID}/" \ -H "Authorization: Bearer $AK_TOKEN" \ -H "Content-Type: application/json" \ -d "{\"providers\": ${PROVIDER_ARRAY}}" 2>/dev/null > /dev/null echo " Provider añadido al outpost." fi # ----------------------------------------------------------- # Verificación # ----------------------------------------------------------- echo "" echo "=========================================" echo " Verificación" echo "=========================================" HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" "${EXTERNAL_HOST}/" 2>/dev/null) REDIRECT=$(curl -sk -o /dev/null -w "%{redirect_url}" "${EXTERNAL_HOST}/" 2>/dev/null) if [ "$HTTP_CODE" = "302" ] && echo "$REDIRECT" | grep -q "/application/o/authorize/"; then echo " OK: ${EXTERNAL_HOST} redirige a Authentik (302)" elif [ "$HTTP_CODE" = "502" ] || [ "$HTTP_CODE" = "503" ]; then echo " WARN: El servicio backend no responde (HTTP ${HTTP_CODE})." echo " La protección Authentik funciona, pero el servicio no está UP." elif [ "$HTTP_CODE" = "200" ] && echo "$REDIRECT" | grep -q "authentik"; then echo " WARN: Redirige a Authentik pero con código 200." echo " Puede que necesites loguearte primero." else echo " INFO: HTTP ${HTTP_CODE}" echo " Verifica manualmente: curl -kI ${EXTERNAL_HOST}" fi echo "" echo "=========================================" echo " Resumen" echo "=========================================" echo " Provider: Provider for ${APP_NAME} (pk=${PROVIDER_PK})" echo " Application: ${APP_NAME} (slug=${APP_SLUG}, pk=${APP_PK})" echo " Outpost: authentik Embedded Outpost (${OUTPOST_UUID})" echo " URL: ${EXTERNAL_HOST}" echo "=========================================" echo "" echo "Listo. El middleware 'ths-authentik@docker' ya debería proteger ${EXTERNAL_HOST}." echo "Si usas CrowdSec también, añade: crowdsec-bouncer@file,ths-authentik@docker"