61134d7834
- create-auth-app.sh: script interactivo para crear providers y aplicaciones en Authentik vía API, automatizando la integración SSO - docker-compose.yml y stack.env: ajustes del stack - README.md: documentación actualizada Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
328 lines
12 KiB
Bash
Executable File
328 lines
12 KiB
Bash
Executable File
#!/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 <dominio> [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 <dominio> [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"
|