mirror of
https://github.com/eduardogsilva/wireguard_webadmin.git
synced 2026-03-17 14:26:18 +00:00
move from authelia implementation to custom authentication gateway
This commit is contained in:
@@ -34,10 +34,14 @@ def build_applications_data():
|
|||||||
def _build_auth_method_entry(method):
|
def _build_auth_method_entry(method):
|
||||||
entry = {'type': method.auth_type}
|
entry = {'type': method.auth_type}
|
||||||
|
|
||||||
if method.auth_type == 'totp':
|
if method.auth_type == 'local_password':
|
||||||
|
entry['session_expiration_minutes'] = method.session_expiration_minutes
|
||||||
|
|
||||||
|
elif method.auth_type == 'totp':
|
||||||
entry['totp_secret'] = method.totp_secret
|
entry['totp_secret'] = method.totp_secret
|
||||||
|
|
||||||
elif method.auth_type == 'oidc':
|
elif method.auth_type == 'oidc':
|
||||||
|
entry['session_expiration_minutes'] = method.session_expiration_minutes
|
||||||
entry['provider'] = method.oidc_provider
|
entry['provider'] = method.oidc_provider
|
||||||
entry['client_id'] = method.oidc_client_id
|
entry['client_id'] = method.oidc_client_id
|
||||||
entry['client_secret'] = method.oidc_client_secret
|
entry['client_secret'] = method.oidc_client_secret
|
||||||
@@ -77,7 +81,7 @@ def build_auth_policies_data():
|
|||||||
for user in GatekeeperUser.objects.all():
|
for user in GatekeeperUser.objects.all():
|
||||||
users[user.username] = {
|
users[user.username] = {
|
||||||
'email': user.email,
|
'email': user.email,
|
||||||
'password_hash': user.password_hash or '',
|
'password_hash': user.password or '',
|
||||||
'totp_secret': user.totp_secret,
|
'totp_secret': user.totp_secret,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
FROM authelia/authelia:latest
|
|
||||||
|
|
||||||
COPY entrypoint.sh /usr/local/bin/authelia-entrypoint.sh
|
|
||||||
RUN chmod +x /usr/local/bin/authelia-entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/local/bin/authelia-entrypoint.sh"]
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
CONFIG_PATH="/config/configuration.yml"
|
|
||||||
WAIT_INTERVAL=5
|
|
||||||
|
|
||||||
echo "==> Waiting for Authelia configuration file..."
|
|
||||||
while [ ! -f "$CONFIG_PATH" ]; do
|
|
||||||
sleep "$WAIT_INTERVAL"
|
|
||||||
done
|
|
||||||
echo "==> Configuration file found: ${CONFIG_PATH}"
|
|
||||||
|
|
||||||
echo "==> Starting Authelia..."
|
|
||||||
authelia --config "$CONFIG_PATH" &
|
|
||||||
AUTHELIA_PID=$!
|
|
||||||
|
|
||||||
sleep 3
|
|
||||||
|
|
||||||
echo "==> Watching ${CONFIG_PATH} for changes..."
|
|
||||||
|
|
||||||
# Function to safely get hash in minimal environments
|
|
||||||
get_hash() {
|
|
||||||
md5sum "$CONFIG_PATH" 2>/dev/null | awk '{print $1}' || echo "error"
|
|
||||||
}
|
|
||||||
|
|
||||||
LAST_HASH=$(get_hash)
|
|
||||||
|
|
||||||
while true; do
|
|
||||||
sleep 3
|
|
||||||
CURRENT_HASH=$(get_hash)
|
|
||||||
|
|
||||||
if [ "$LAST_HASH" != "$CURRENT_HASH" ]; then
|
|
||||||
echo "==> Configuration change detected, restarting Authelia..."
|
|
||||||
LAST_HASH="$CURRENT_HASH"
|
|
||||||
|
|
||||||
kill "$AUTHELIA_PID" 2>/dev/null || true
|
|
||||||
wait "$AUTHELIA_PID" 2>/dev/null || true
|
|
||||||
|
|
||||||
authelia --config "$CONFIG_PATH" &
|
|
||||||
AUTHELIA_PID=$!
|
|
||||||
echo "==> Authelia restarted with PID ${AUTHELIA_PID}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
@@ -1,9 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Reads JSON config files exported by Django and generates:
|
Reads JSON config files exported by Django and generates /etc/caddy/Caddyfile.
|
||||||
- /etc/caddy/Caddyfile
|
|
||||||
- /authelia_config/configuration.yml
|
|
||||||
- /authelia_config/users_database.yml
|
|
||||||
|
|
||||||
Expected input files in /caddy_json_export/:
|
Expected input files in /caddy_json_export/:
|
||||||
- wireguard_webadmin.json (required, generated on container startup)
|
- wireguard_webadmin.json (required, generated on container startup)
|
||||||
@@ -14,20 +11,13 @@ Expected input files in /caddy_json_export/:
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import secrets
|
from urllib.parse import urlparse
|
||||||
import string
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
JSON_DIR = os.environ.get("JSON_DIR", "/caddy_json_export")
|
JSON_DIR = os.environ.get("JSON_DIR", "/caddy_json_export")
|
||||||
CADDYFILE_PATH = os.environ.get("CADDYFILE_PATH", "/etc/caddy/Caddyfile")
|
CADDYFILE_PATH = os.environ.get("CADDYFILE_PATH", "/etc/caddy/Caddyfile")
|
||||||
AUTHELIA_CONFIG_DIR = os.environ.get("AUTHELIA_CONFIG_DIR", "/authelia_config")
|
AUTH_GATEWAY_INTERNAL_URL = "http://wireguard-webadmin-auth-gateway:9091"
|
||||||
AUTHELIA_CONFIG_PATH = os.path.join(AUTHELIA_CONFIG_DIR, "configuration.yml")
|
AUTH_GATEWAY_PORTAL_PATH = "/auth-gateway"
|
||||||
AUTHELIA_USERS_PATH = os.path.join(AUTHELIA_CONFIG_DIR, "users_database.yml")
|
AUTH_GATEWAY_CHECK_URI = "/auth/check"
|
||||||
AUTHELIA_SECRETS_DIR = os.path.join(AUTHELIA_CONFIG_DIR, "secrets")
|
|
||||||
|
|
||||||
AUTHELIA_INTERNAL_URL = "http://wireguard-webadmin-authelia:9091"
|
|
||||||
AUTHELIA_PORTAL_PATH = "/authelia"
|
|
||||||
|
|
||||||
|
|
||||||
def load_json(filename):
|
def load_json(filename):
|
||||||
@@ -38,25 +28,7 @@ def load_json(filename):
|
|||||||
return json.load(json_file)
|
return json.load(json_file)
|
||||||
|
|
||||||
|
|
||||||
def generate_secret(length=64):
|
|
||||||
alphabet = string.ascii_letters + string.digits
|
|
||||||
return "".join(secrets.choice(alphabet) for char_index in range(length))
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_secret(name):
|
|
||||||
os.makedirs(AUTHELIA_SECRETS_DIR, exist_ok=True)
|
|
||||||
secret_path = os.path.join(AUTHELIA_SECRETS_DIR, name)
|
|
||||||
if os.path.exists(secret_path):
|
|
||||||
with open(secret_path, "r", encoding="utf-8") as secret_file:
|
|
||||||
return secret_file.read().strip()
|
|
||||||
secret_value = generate_secret()
|
|
||||||
with open(secret_path, "w", encoding="utf-8") as secret_file:
|
|
||||||
secret_file.write(secret_value)
|
|
||||||
return secret_value
|
|
||||||
|
|
||||||
|
|
||||||
def collect_all_applications():
|
def collect_all_applications():
|
||||||
"""Merge entries from wireguard_webadmin.json and applications.json."""
|
|
||||||
apps = []
|
apps = []
|
||||||
|
|
||||||
webadmin_data = load_json("wireguard_webadmin.json")
|
webadmin_data = load_json("wireguard_webadmin.json")
|
||||||
@@ -70,440 +42,117 @@ def collect_all_applications():
|
|||||||
return apps
|
return apps
|
||||||
|
|
||||||
|
|
||||||
|
def split_upstream(upstream):
|
||||||
|
"""Return (base_url, upstream_path) where base_url has no path component.
|
||||||
|
Upstreams without an explicit http(s):// scheme are returned as-is with no path."""
|
||||||
|
if not upstream.startswith("http://") and not upstream.startswith("https://"):
|
||||||
|
return upstream, ""
|
||||||
|
parsed = urlparse(upstream)
|
||||||
|
base = f"{parsed.scheme}://{parsed.netloc}"
|
||||||
|
path = parsed.path.rstrip("/")
|
||||||
|
return base, path
|
||||||
|
|
||||||
|
|
||||||
def build_caddyfile(apps, auth_policies, routes):
|
def build_caddyfile(apps, auth_policies, routes):
|
||||||
|
policies = auth_policies.get("policies", {}) if auth_policies else {}
|
||||||
|
route_entries = routes.get("entries", {}) if routes else {}
|
||||||
lines = []
|
lines = []
|
||||||
has_authelia = auth_policies is not None
|
|
||||||
policies = auth_policies.get("policies", {}) if has_authelia else {}
|
|
||||||
|
|
||||||
def get_policy_type(policy_name):
|
def get_policy_type(policy_name):
|
||||||
if policy_name and policy_name in policies:
|
if policy_name and policy_name in policies:
|
||||||
return policies[policy_name].get("policy_type", "bypass")
|
return policies[policy_name].get("policy_type", "bypass")
|
||||||
return "bypass"
|
return "deny"
|
||||||
|
|
||||||
def emit_authelia_portal():
|
def emit_auth_portal():
|
||||||
lines.append(f" handle_path {AUTHELIA_PORTAL_PATH}/* {{")
|
lines.append(f" handle_path {AUTH_GATEWAY_PORTAL_PATH}/* {{")
|
||||||
lines.append(f" reverse_proxy {AUTHELIA_INTERNAL_URL}")
|
lines.append(f" reverse_proxy {AUTH_GATEWAY_INTERNAL_URL}")
|
||||||
lines.append(f" }}")
|
lines.append(" }")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
def handle_open(matcher):
|
||||||
|
if matcher == "*":
|
||||||
|
return " handle {"
|
||||||
|
return f" handle {matcher} {{"
|
||||||
|
|
||||||
|
def emit_reverse_proxy(base, upstream_path, indent=" "):
|
||||||
|
if upstream_path:
|
||||||
|
lines.append(f"{indent}rewrite * {upstream_path}{{uri}}")
|
||||||
|
lines.append(f"{indent}reverse_proxy {base}")
|
||||||
|
|
||||||
|
def emit_protected_handle(path_matcher, base, upstream_path):
|
||||||
|
lines.append(handle_open(path_matcher))
|
||||||
|
lines.append(f" forward_auth {AUTH_GATEWAY_INTERNAL_URL} {{")
|
||||||
|
lines.append(f" uri {AUTH_GATEWAY_CHECK_URI}")
|
||||||
|
lines.append(" copy_headers X-Auth-User X-Auth-Email X-Auth-Groups X-Auth-Factors X-Auth-Policy")
|
||||||
|
lines.append(" }")
|
||||||
|
emit_reverse_proxy(base, upstream_path)
|
||||||
|
lines.append(" }")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
for app in apps:
|
for app in apps:
|
||||||
app_id = app.get("id", "unknown")
|
|
||||||
hosts = app.get("hosts", [])
|
hosts = app.get("hosts", [])
|
||||||
upstream = app.get("upstream", "")
|
upstream = app.get("upstream", "")
|
||||||
static_routes = app.get("static_routes", [])
|
static_routes = app.get("static_routes", [])
|
||||||
|
app_id = app.get("id", "")
|
||||||
|
|
||||||
if not hosts or not upstream:
|
if not hosts or not upstream:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
host_list = ", ".join(hosts)
|
base, upstream_path = split_upstream(upstream)
|
||||||
lines.append(f"{host_list} {{")
|
|
||||||
|
|
||||||
if has_authelia and app_id == "wireguard_webadmin":
|
lines.append(f"{', '.join(hosts)} {{")
|
||||||
emit_authelia_portal()
|
emit_auth_portal()
|
||||||
|
|
||||||
for static_route in static_routes:
|
for static_route in static_routes:
|
||||||
path_prefix = static_route.get("path_prefix", "")
|
path_prefix = static_route.get("path_prefix", "")
|
||||||
root_dir = static_route.get("root", "")
|
root_dir = static_route.get("root", "")
|
||||||
strip_prefix = static_route.get("strip_prefix", "")
|
|
||||||
cache_control = static_route.get("cache_control", "")
|
cache_control = static_route.get("cache_control", "")
|
||||||
|
|
||||||
lines.append(f" handle_path {path_prefix}/* {{")
|
lines.append(f" handle_path {path_prefix}/* {{")
|
||||||
lines.append(f" root * {root_dir}")
|
lines.append(f" root * {root_dir}")
|
||||||
lines.append(f" file_server")
|
lines.append(" file_server")
|
||||||
if cache_control:
|
if cache_control:
|
||||||
lines.append(f" header Cache-Control \"{cache_control}\"")
|
lines.append(f" header Cache-Control \"{cache_control}\"")
|
||||||
lines.append(f" }}")
|
lines.append(" }")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
app_routes = {}
|
app_route_data = route_entries.get(app_id)
|
||||||
app_default_policy = None
|
if app_route_data is None:
|
||||||
if routes:
|
emit_reverse_proxy(base, upstream_path, indent=" ")
|
||||||
route_entries = routes.get("entries", {})
|
lines.append("}")
|
||||||
if app_id in route_entries:
|
|
||||||
app_route_data = route_entries[app_id]
|
|
||||||
app_default_policy = app_route_data.get("default_policy")
|
|
||||||
for route in app_route_data.get("routes", []):
|
|
||||||
app_routes[route.get("path_prefix", "")] = route.get("policy", "")
|
|
||||||
|
|
||||||
default_policy_type = get_policy_type(app_default_policy)
|
|
||||||
|
|
||||||
# When the default policy is deny, use handle blocks for specific
|
|
||||||
# non-deny routes and a catch-all respond 403 at Caddy level,
|
|
||||||
# avoiding an unnecessary Authelia round-trip.
|
|
||||||
if has_authelia and default_policy_type == "deny":
|
|
||||||
has_protected_routes = any(
|
|
||||||
get_policy_type(pn) not in ("bypass", "deny")
|
|
||||||
for pn in app_routes.values()
|
|
||||||
)
|
|
||||||
if has_protected_routes:
|
|
||||||
emit_authelia_portal()
|
|
||||||
|
|
||||||
for path_prefix, policy_name in app_routes.items():
|
|
||||||
ptype = get_policy_type(policy_name)
|
|
||||||
if ptype == "bypass":
|
|
||||||
lines.append(f" handle {path_prefix}/* {{")
|
|
||||||
lines.append(f" reverse_proxy {upstream}")
|
|
||||||
lines.append(f" }}")
|
|
||||||
lines.append("")
|
|
||||||
elif ptype == "deny":
|
|
||||||
lines.append(f" handle {path_prefix}/* {{")
|
|
||||||
lines.append(f" respond 403")
|
|
||||||
lines.append(f" }}")
|
|
||||||
lines.append("")
|
|
||||||
else:
|
|
||||||
lines.append(f" handle {path_prefix}/* {{")
|
|
||||||
lines.append(f" forward_auth {AUTHELIA_INTERNAL_URL} {{")
|
|
||||||
lines.append(f" uri {AUTHELIA_PORTAL_PATH}/api/authz/forward-auth")
|
|
||||||
lines.append(f" copy_headers Remote-User Remote-Groups Remote-Name Remote-Email")
|
|
||||||
lines.append(f" }}")
|
|
||||||
lines.append(f" reverse_proxy {upstream}")
|
|
||||||
lines.append(f" }}")
|
|
||||||
lines.append("")
|
|
||||||
lines.append(f" respond 403")
|
|
||||||
lines.append(f"}}")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# For bypass/protected default policy: emit explicit deny blocks for
|
route_list = sorted(app_route_data.get("routes", []), key=lambda route: len(route.get("path_prefix", "")), reverse=True)
|
||||||
# any per-route deny entries before the forward_auth check.
|
for route in route_list:
|
||||||
for path_prefix, policy_name in app_routes.items():
|
path_prefix = route.get("path_prefix", "/")
|
||||||
if get_policy_type(policy_name) == "deny":
|
policy_type = get_policy_type(route.get("policy"))
|
||||||
lines.append(f" handle {path_prefix}/* {{")
|
matcher = f"{path_prefix}*"
|
||||||
lines.append(f" respond 403")
|
if policy_type == "bypass":
|
||||||
lines.append(f" }}")
|
lines.append(handle_open(matcher))
|
||||||
|
emit_reverse_proxy(base, upstream_path)
|
||||||
|
lines.append(" }")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
elif policy_type == "deny":
|
||||||
needs_auth = False
|
lines.append(handle_open(matcher))
|
||||||
if has_authelia and auth_policies:
|
lines.append(" respond 403")
|
||||||
if default_policy_type not in ("bypass", "deny"):
|
lines.append(" }")
|
||||||
needs_auth = True
|
|
||||||
for path_prefix, policy_name in app_routes.items():
|
|
||||||
if get_policy_type(policy_name) not in ("bypass", "deny"):
|
|
||||||
needs_auth = True
|
|
||||||
|
|
||||||
if needs_auth:
|
|
||||||
# Expose the Authelia portal on this domain so session cookies can
|
|
||||||
# be set with the correct scope (authelia_url = this domain).
|
|
||||||
if app_id != "wireguard_webadmin":
|
|
||||||
emit_authelia_portal()
|
|
||||||
|
|
||||||
for path_prefix, policy_name in app_routes.items():
|
|
||||||
if get_policy_type(policy_name) == "bypass":
|
|
||||||
lines.append(f" @bypass_{_sanitize_id(path_prefix)} path {path_prefix}*")
|
|
||||||
lines.append(f" skip_log @bypass_{_sanitize_id(path_prefix)}")
|
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
else:
|
||||||
|
emit_protected_handle(matcher, base, upstream_path)
|
||||||
|
|
||||||
lines.append(f" forward_auth {AUTHELIA_INTERNAL_URL} {{")
|
default_policy_type = get_policy_type(app_route_data.get("default_policy"))
|
||||||
lines.append(f" uri {AUTHELIA_PORTAL_PATH}/api/authz/forward-auth")
|
if default_policy_type == "bypass":
|
||||||
lines.append(f" copy_headers Remote-User Remote-Groups Remote-Name Remote-Email")
|
emit_reverse_proxy(base, upstream_path, indent=" ")
|
||||||
lines.append(f" }}")
|
elif default_policy_type == "deny":
|
||||||
lines.append("")
|
lines.append(" respond 403")
|
||||||
|
else:
|
||||||
lines.append(f" reverse_proxy {upstream}")
|
emit_protected_handle("*", base, upstream_path)
|
||||||
lines.append(f"}}")
|
lines.append("}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
def _sanitize_id(value):
|
|
||||||
return value.strip("/").replace("/", "_").replace("-", "_")
|
|
||||||
|
|
||||||
|
|
||||||
def _collect_session_cookies(apps, routes, policies, server_address):
|
|
||||||
"""Build session.cookies entries for Authelia.
|
|
||||||
|
|
||||||
Authelia v4.37+ requires a session.cookies entry for every domain managed
|
|
||||||
via forward_auth, and authelia_url must share the cookie scope with its
|
|
||||||
domain. For multi-domain gateways this means each domain must expose the
|
|
||||||
Authelia portal under its own hostname — Caddy handles this by routing
|
|
||||||
/authelia/* to the Authelia backend for each protected app.
|
|
||||||
|
|
||||||
Only apps with at least one protected (non-bypass, non-deny) route are
|
|
||||||
included. Deny is handled at the Caddy level; bypass needs no session.
|
|
||||||
"""
|
|
||||||
def get_policy_type(policy_name):
|
|
||||||
if policy_name and policy_name in policies:
|
|
||||||
return policies[policy_name].get("policy_type", "bypass")
|
|
||||||
return "bypass"
|
|
||||||
|
|
||||||
route_entries = routes.get("entries", {}) if routes else {}
|
|
||||||
seen = {server_address}
|
|
||||||
cookies = [
|
|
||||||
{
|
|
||||||
"domain": server_address,
|
|
||||||
"authelia_url": f"https://{server_address}{AUTHELIA_PORTAL_PATH}",
|
|
||||||
"default_redirection_url": f"https://{server_address}",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
for app in apps:
|
|
||||||
app_id = app.get("id", "")
|
|
||||||
hosts = app.get("hosts", [])
|
|
||||||
|
|
||||||
if app_id == "wireguard_webadmin":
|
|
||||||
# Extra hostnames for the main app already have /authelia/* routed
|
|
||||||
# in Caddy — add their session cookies pointing to themselves.
|
|
||||||
for host in hosts:
|
|
||||||
if host not in seen:
|
|
||||||
seen.add(host)
|
|
||||||
cookies.append({
|
|
||||||
"domain": host,
|
|
||||||
"authelia_url": f"https://{host}{AUTHELIA_PORTAL_PATH}",
|
|
||||||
"default_redirection_url": f"https://{host}",
|
|
||||||
})
|
|
||||||
continue
|
|
||||||
|
|
||||||
app_route_data = route_entries.get(app_id, {})
|
|
||||||
default_ptype = get_policy_type(app_route_data.get("default_policy"))
|
|
||||||
needs_auth = default_ptype not in ("bypass", "deny")
|
|
||||||
if not needs_auth:
|
|
||||||
for route in app_route_data.get("routes", []):
|
|
||||||
if get_policy_type(route.get("policy", "")) not in ("bypass", "deny"):
|
|
||||||
needs_auth = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if not needs_auth:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for host in hosts:
|
|
||||||
if host not in seen:
|
|
||||||
seen.add(host)
|
|
||||||
cookies.append({
|
|
||||||
"domain": host,
|
|
||||||
"authelia_url": f"https://{host}{AUTHELIA_PORTAL_PATH}",
|
|
||||||
"default_redirection_url": f"https://{host}",
|
|
||||||
})
|
|
||||||
|
|
||||||
return cookies
|
|
||||||
|
|
||||||
|
|
||||||
def build_authelia_config(auth_policies, routes, apps):
|
|
||||||
server_address = os.environ.get("SERVER_ADDRESS", "localhost")
|
|
||||||
|
|
||||||
jwt_secret = get_or_create_secret("jwt_secret")
|
|
||||||
session_secret = get_or_create_secret("session_secret")
|
|
||||||
storage_encryption_key = get_or_create_secret("storage_encryption_key")
|
|
||||||
|
|
||||||
session_cookies = _collect_session_cookies(
|
|
||||||
apps, routes, auth_policies.get("policies", {}), server_address
|
|
||||||
)
|
|
||||||
|
|
||||||
config = {
|
|
||||||
"server": {
|
|
||||||
"address": "tcp://0.0.0.0:9091",
|
|
||||||
},
|
|
||||||
"log": {
|
|
||||||
"level": "info",
|
|
||||||
},
|
|
||||||
"identity_validation": {
|
|
||||||
"reset_password": {
|
|
||||||
"jwt_secret": jwt_secret,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"authentication_backend": {
|
|
||||||
"file": {
|
|
||||||
"path": "/config/users_database.yml",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"session": {
|
|
||||||
"secret": session_secret,
|
|
||||||
"cookies": session_cookies,
|
|
||||||
},
|
|
||||||
"storage": {
|
|
||||||
"encryption_key": storage_encryption_key,
|
|
||||||
"local": {
|
|
||||||
"path": "/data/db.sqlite3",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"notifier": {
|
|
||||||
"filesystem": {
|
|
||||||
"filename": "/data/notification.txt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
"access_control": build_access_control_rules(auth_policies, routes, apps),
|
|
||||||
}
|
|
||||||
|
|
||||||
identity_providers = build_identity_providers(auth_policies, server_address)
|
|
||||||
if identity_providers:
|
|
||||||
config["identity_providers"] = identity_providers
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
|
|
||||||
def build_access_control_rules(auth_policies, routes, apps):
|
|
||||||
if not auth_policies or not routes:
|
|
||||||
return {"default_policy": "bypass", "rules": []}
|
|
||||||
|
|
||||||
policies = auth_policies.get("policies", {})
|
|
||||||
route_entries = routes.get("entries", {})
|
|
||||||
|
|
||||||
host_map = {}
|
|
||||||
for app in apps:
|
|
||||||
app_id = app.get("id", "unknown")
|
|
||||||
host_map[app_id] = app.get("hosts", [])
|
|
||||||
|
|
||||||
rules = []
|
|
||||||
|
|
||||||
for app_id, route_data in route_entries.items():
|
|
||||||
app_hosts = host_map.get(app_id, [])
|
|
||||||
if not app_hosts:
|
|
||||||
continue
|
|
||||||
|
|
||||||
domain_list = list(app_hosts)
|
|
||||||
|
|
||||||
for route in route_data.get("routes", []):
|
|
||||||
policy_name = route.get("policy", "")
|
|
||||||
path_prefix = route.get("path_prefix", "")
|
|
||||||
|
|
||||||
if policy_name not in policies:
|
|
||||||
continue
|
|
||||||
|
|
||||||
policy_data = policies[policy_name]
|
|
||||||
policy_type = policy_data.get("policy_type", "bypass")
|
|
||||||
|
|
||||||
if policy_type == "bypass":
|
|
||||||
authelia_policy = "bypass"
|
|
||||||
elif policy_type == "deny":
|
|
||||||
authelia_policy = "deny"
|
|
||||||
else:
|
|
||||||
authelia_policy = "two_factor"
|
|
||||||
|
|
||||||
rule = {
|
|
||||||
"domain": domain_list,
|
|
||||||
"policy": authelia_policy,
|
|
||||||
"resources": [f"^{path_prefix}.*$"],
|
|
||||||
}
|
|
||||||
|
|
||||||
groups = policy_data.get("groups", [])
|
|
||||||
if groups and authelia_policy not in ("bypass", "deny"):
|
|
||||||
rule["subject"] = [f"group:{g}" for g in groups]
|
|
||||||
|
|
||||||
rules.append(rule)
|
|
||||||
|
|
||||||
default_policy_name = route_data.get("default_policy")
|
|
||||||
if default_policy_name and default_policy_name in policies:
|
|
||||||
default_policy_data = policies[default_policy_name]
|
|
||||||
default_type = default_policy_data.get("policy_type", "bypass")
|
|
||||||
|
|
||||||
if default_type == "bypass":
|
|
||||||
authelia_default = "bypass"
|
|
||||||
elif default_type == "deny":
|
|
||||||
authelia_default = "deny"
|
|
||||||
else:
|
|
||||||
authelia_default = "two_factor"
|
|
||||||
|
|
||||||
default_rule = {
|
|
||||||
"domain": domain_list,
|
|
||||||
"policy": authelia_default,
|
|
||||||
}
|
|
||||||
|
|
||||||
groups = default_policy_data.get("groups", [])
|
|
||||||
if groups and authelia_default not in ("bypass", "deny"):
|
|
||||||
default_rule["subject"] = [f"group:{g}" for g in groups]
|
|
||||||
|
|
||||||
rules.append(default_rule)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"default_policy": "two_factor" if not rules else "deny",
|
|
||||||
"rules": rules,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_identity_providers(auth_policies, server_address):
|
|
||||||
if not auth_policies:
|
|
||||||
return None
|
|
||||||
|
|
||||||
auth_methods = auth_policies.get("auth_methods", {})
|
|
||||||
oidc_clients = []
|
|
||||||
|
|
||||||
for method_name, method in auth_methods.items():
|
|
||||||
if method.get("type") != "oidc":
|
|
||||||
continue
|
|
||||||
|
|
||||||
client_id = method.get("client_id", "")
|
|
||||||
client_secret = method.get("client_secret", "")
|
|
||||||
if not client_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
client = {
|
|
||||||
"client_id": client_id,
|
|
||||||
"client_secret": client_secret,
|
|
||||||
"authorization_policy": "two_factor",
|
|
||||||
"redirect_uris": [
|
|
||||||
f"https://{server_address}{AUTHELIA_PORTAL_PATH}/oidc/callback",
|
|
||||||
],
|
|
||||||
"scopes": ["openid", "profile", "email", "groups"],
|
|
||||||
}
|
|
||||||
oidc_clients.append(client)
|
|
||||||
|
|
||||||
if not oidc_clients:
|
|
||||||
return None
|
|
||||||
|
|
||||||
hmac_secret = get_or_create_secret("oidc_hmac_secret")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"oidc": {
|
|
||||||
"hmac_secret": hmac_secret,
|
|
||||||
"clients": oidc_clients,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
DUMMY_USER = {
|
|
||||||
"_dummy_setup_user": {
|
|
||||||
"disabled": True,
|
|
||||||
"displayname": "Dummy Setup User",
|
|
||||||
"password": "$argon2id$v=19$m=65536,t=3,p=4$Nklqa1J5a3ZweDhlZnNlUw$5D8WJ+sT20eXj1U10qNnS2Ew/M40B8v1/37X2b1lG0I",
|
|
||||||
"email": "dummy@localhost",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
def build_users_database(auth_policies):
|
|
||||||
if not auth_policies:
|
|
||||||
return {"users": DUMMY_USER}
|
|
||||||
|
|
||||||
users_data = auth_policies.get("users", {})
|
|
||||||
groups_data = auth_policies.get("groups", {})
|
|
||||||
|
|
||||||
user_groups = {}
|
|
||||||
for group_name, group_info in groups_data.items():
|
|
||||||
for username in group_info.get("users", []):
|
|
||||||
if username not in user_groups:
|
|
||||||
user_groups[username] = []
|
|
||||||
user_groups[username].append(group_name)
|
|
||||||
|
|
||||||
users = {}
|
|
||||||
for username, user_info in users_data.items():
|
|
||||||
user_entry = {
|
|
||||||
"displayname": username,
|
|
||||||
"email": user_info.get("email", f"{username}@localhost"),
|
|
||||||
"groups": user_groups.get(username, []),
|
|
||||||
}
|
|
||||||
password_hash = user_info.get("password_hash", "")
|
|
||||||
if password_hash:
|
|
||||||
user_entry["password"] = password_hash
|
|
||||||
|
|
||||||
users[username] = user_entry
|
|
||||||
|
|
||||||
if not users:
|
|
||||||
users = DUMMY_USER.copy()
|
|
||||||
|
|
||||||
return {"users": users}
|
|
||||||
|
|
||||||
|
|
||||||
class _NoAliasDumper(yaml.SafeDumper):
|
|
||||||
"""YAML dumper that never emits anchors/aliases."""
|
|
||||||
def ignore_aliases(self, data):
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def write_yaml(filepath, data):
|
|
||||||
os.makedirs(os.path.dirname(filepath), exist_ok=True)
|
|
||||||
with open(filepath, "w", encoding="utf-8") as yaml_file:
|
|
||||||
yaml.dump(data, yaml_file, Dumper=_NoAliasDumper, default_flow_style=False, allow_unicode=True, sort_keys=False)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
apps = collect_all_applications()
|
apps = collect_all_applications()
|
||||||
auth_policies = load_json("auth_policies.json")
|
auth_policies = load_json("auth_policies.json")
|
||||||
@@ -515,17 +164,6 @@ def main():
|
|||||||
caddyfile.write(caddyfile_content)
|
caddyfile.write(caddyfile_content)
|
||||||
print(f"Caddyfile written to {CADDYFILE_PATH}")
|
print(f"Caddyfile written to {CADDYFILE_PATH}")
|
||||||
|
|
||||||
if auth_policies:
|
|
||||||
authelia_config = build_authelia_config(auth_policies, routes, apps)
|
|
||||||
write_yaml(AUTHELIA_CONFIG_PATH, authelia_config)
|
|
||||||
print(f"Authelia configuration written to {AUTHELIA_CONFIG_PATH}")
|
|
||||||
|
|
||||||
users_db = build_users_database(auth_policies)
|
|
||||||
write_yaml(AUTHELIA_USERS_PATH, users_db)
|
|
||||||
print(f"Authelia users database written to {AUTHELIA_USERS_PATH}")
|
|
||||||
else:
|
|
||||||
print("No auth_policies.json found, skipping Authelia config generation.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- static_volume:/static
|
- static_volume:/static
|
||||||
- caddy_json_export:/caddy_json_export
|
- caddy_json_export:/caddy_json_export
|
||||||
- authelia_config:/authelia_config
|
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
ports:
|
ports:
|
||||||
@@ -92,20 +91,23 @@ services:
|
|||||||
- "443:443"
|
- "443:443"
|
||||||
depends_on:
|
depends_on:
|
||||||
- wireguard-webadmin
|
- wireguard-webadmin
|
||||||
|
- wireguard-webadmin-auth-gateway
|
||||||
|
|
||||||
wireguard-webadmin-authelia:
|
wireguard-webadmin-auth-gateway:
|
||||||
container_name: wireguard-webadmin-authelia
|
container_name: wireguard-webadmin-auth-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
build:
|
build:
|
||||||
context: ./containers/authelia
|
context: ./containers/auth-gateway
|
||||||
dockerfile: Dockerfile-authelia
|
dockerfile: Dockerfile-auth-gateway
|
||||||
environment:
|
environment:
|
||||||
- TZ=${TIMEZONE}
|
- TZ=${TIMEZONE}
|
||||||
|
- AUTH_GATEWAY_CONFIG_DIR=/caddy_json_export
|
||||||
|
- AUTH_GATEWAY_DATABASE_PATH=/data/auth-gateway.sqlite3
|
||||||
volumes:
|
volumes:
|
||||||
- authelia_config:/config
|
- caddy_json_export:/caddy_json_export
|
||||||
- authelia_data:/data
|
- auth_gateway_data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- wireguard-webadmin-caddy
|
- wireguard-webadmin
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
static_volume:
|
static_volume:
|
||||||
@@ -114,7 +116,6 @@ volumes:
|
|||||||
app_secrets:
|
app_secrets:
|
||||||
rrd_data:
|
rrd_data:
|
||||||
caddy_json_export:
|
caddy_json_export:
|
||||||
authelia_config:
|
auth_gateway_data:
|
||||||
authelia_data:
|
|
||||||
caddy_data:
|
caddy_data:
|
||||||
caddy_config:
|
caddy_config:
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- static_volume:/static
|
- static_volume:/static
|
||||||
- caddy_json_export:/caddy_json_export
|
- caddy_json_export:/caddy_json_export
|
||||||
- authelia_config:/authelia_config
|
|
||||||
- caddy_data:/data
|
- caddy_data:/data
|
||||||
- caddy_config:/config
|
- caddy_config:/config
|
||||||
ports:
|
ports:
|
||||||
@@ -102,20 +101,24 @@ services:
|
|||||||
- "443:443"
|
- "443:443"
|
||||||
depends_on:
|
depends_on:
|
||||||
- wireguard-webadmin
|
- wireguard-webadmin
|
||||||
|
- wireguard-webadmin-auth-gateway
|
||||||
|
|
||||||
wireguard-webadmin-authelia:
|
wireguard-webadmin-auth-gateway:
|
||||||
container_name: wireguard-webadmin-authelia
|
container_name: wireguard-webadmin-auth-gateway
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
build:
|
build:
|
||||||
context: ./containers/authelia
|
context: ./containers/auth-gateway
|
||||||
dockerfile: Dockerfile-authelia
|
dockerfile: Dockerfile-auth-gateway
|
||||||
environment:
|
environment:
|
||||||
- TZ=${TIMEZONE}
|
- TZ=${TIMEZONE}
|
||||||
|
- AUTH_GATEWAY_CONFIG_DIR=/caddy_json_export
|
||||||
|
- AUTH_GATEWAY_DATABASE_PATH=/data/auth-gateway.sqlite3
|
||||||
|
- AUTH_GATEWAY_SECURE_COOKIES=false
|
||||||
volumes:
|
volumes:
|
||||||
- authelia_config:/config
|
- caddy_json_export:/caddy_json_export
|
||||||
- authelia_data:/data
|
- auth_gateway_data:/data
|
||||||
depends_on:
|
depends_on:
|
||||||
- wireguard-webadmin-caddy
|
- wireguard-webadmin
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
static_volume:
|
static_volume:
|
||||||
@@ -124,7 +127,6 @@ volumes:
|
|||||||
app_secrets:
|
app_secrets:
|
||||||
rrd_data:
|
rrd_data:
|
||||||
caddy_json_export:
|
caddy_json_export:
|
||||||
authelia_config:
|
auth_gateway_data:
|
||||||
authelia_data:
|
|
||||||
caddy_data:
|
caddy_data:
|
||||||
caddy_config:
|
caddy_config:
|
||||||
|
|||||||
Reference in New Issue
Block a user