Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23c559d7f6 | ||
|
|
80b792ce8c | ||
|
|
e40a4d0c72 | ||
|
|
03d580999a | ||
|
|
a28fed03ed | ||
|
|
99a69257e6 | ||
|
|
f0f455c7dd | ||
|
|
ede07b72e8 | ||
|
|
4662797118 | ||
|
|
505bfec45a | ||
|
|
a3b5811b83 | ||
|
|
f58f979b3b | ||
|
|
c4a3623e55 | ||
|
|
dd3693095f | ||
|
|
4273e62d68 | ||
|
|
5d8a582e1e | ||
|
|
31714fe357 | ||
|
|
2e22f98eb4 | ||
|
|
69186d807e | ||
|
|
719504a399 | ||
|
|
1059e430a0 |
@@ -45,6 +45,7 @@ ArgoCD homelab project
|
||||
| **amnezia** | [](https://ag.hexor.cy/applications/argocd/amnezia) |
|
||||
| **comfyui** | [](https://ag.hexor.cy/applications/argocd/comfyui) |
|
||||
| **doka2-lobby-list** | [](https://ag.hexor.cy/applications/argocd/doka2-lobby-list) |
|
||||
| **firefly-iii** | [](https://ag.hexor.cy/applications/argocd/firefly-iii) |
|
||||
| **furumi** | [](https://ag.hexor.cy/applications/argocd/furumi) |
|
||||
| **gitea** | [](https://ag.hexor.cy/applications/argocd/gitea) |
|
||||
| **greece-notifier** | [](https://ag.hexor.cy/applications/argocd/greece-notifier) |
|
||||
|
||||
@@ -10,12 +10,19 @@ data:
|
||||
|
||||
PORT="${1:-5847}"
|
||||
VPN_CIDR="${2:-10.8.0.0/16}"
|
||||
POLICY_FILE="${3:-/run/amnezia/policy/policy.conf}"
|
||||
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
|
||||
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
|
||||
POLICY_CHAIN_B="AMNEZIAWG-POLICY-B"
|
||||
IPTABLES=(iptables -w 30)
|
||||
|
||||
external_interface() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
}
|
||||
|
||||
ensure_insert_rule() {
|
||||
ensure_rule() {
|
||||
local mode="$1"
|
||||
shift
|
||||
local table_args=()
|
||||
if [ "${1:-}" = "-t" ]; then
|
||||
table_args=("$1" "$2")
|
||||
@@ -25,8 +32,8 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
iptables "${table_args[@]}" -I "${chain}" 1 "$@"
|
||||
if ! "${IPTABLES[@]}" "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
"${IPTABLES[@]}" "${table_args[@]}" "${mode}" "${chain}" "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -40,23 +47,88 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
while iptables "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
while "${IPTABLES[@]}" "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
true
|
||||
done
|
||||
}
|
||||
|
||||
ensure_append_rule() {
|
||||
local table_args=()
|
||||
if [ "${1:-}" = "-t" ]; then
|
||||
table_args=("$1" "$2")
|
||||
shift 2
|
||||
ensure_chain() {
|
||||
"${IPTABLES[@]}" -N "$1" >/dev/null 2>&1 || true
|
||||
}
|
||||
|
||||
valid_ipv4_32() {
|
||||
local value="$1"
|
||||
[[ "${value}" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}/32$ ]] || return 1
|
||||
local address="${value%/32}"
|
||||
local octet
|
||||
IFS=. read -r -a octets <<< "${address}"
|
||||
[ "${#octets[@]}" -eq 4 ] || return 1
|
||||
for octet in "${octets[@]}"; do
|
||||
[[ "${octet}" =~ ^[0-9]+$ ]] || return 1
|
||||
[ "${octet}" -le 255 ] || return 1
|
||||
done
|
||||
}
|
||||
|
||||
routed_through_awg0() {
|
||||
ip -4 route get "${1%/32}" 2>/dev/null | grep -Eq '(^|[[:space:]])dev awg0([[:space:]]|$)'
|
||||
}
|
||||
|
||||
active_policy_chain() {
|
||||
"${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null \
|
||||
| awk '$1 == "-A" && $3 == "-j" && $4 ~ /^AMNEZIAWG-POLICY-[AB]$/ { print $4; exit }'
|
||||
}
|
||||
|
||||
apply_policy() {
|
||||
local active inactive source destination extra
|
||||
active="$(active_policy_chain || true)"
|
||||
if [ "${active}" = "${POLICY_CHAIN_A}" ]; then
|
||||
inactive="${POLICY_CHAIN_B}"
|
||||
else
|
||||
inactive="${POLICY_CHAIN_A}"
|
||||
fi
|
||||
|
||||
local chain="$1"
|
||||
shift
|
||||
ensure_chain "${inactive}"
|
||||
"${IPTABLES[@]}" -F "${inactive}"
|
||||
|
||||
if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then
|
||||
iptables "${table_args[@]}" -A "${chain}" "$@"
|
||||
if [ -f "${POLICY_FILE}" ]; then
|
||||
while read -r source destination extra; do
|
||||
[ -n "${source:-}" ] || continue
|
||||
[[ "${source}" == \#* ]] && continue
|
||||
if [ -n "${extra:-}" ] \
|
||||
|| ! valid_ipv4_32 "${source}" \
|
||||
|| ! valid_ipv4_32 "${destination:-}" \
|
||||
|| ! routed_through_awg0 "${source}" \
|
||||
|| ! routed_through_awg0 "${destination}"; then
|
||||
echo "Invalid or out-of-tunnel policy line: ${source:-} ${destination:-} ${extra:-}" >&2
|
||||
"${IPTABLES[@]}" -F "${inactive}"
|
||||
return 1
|
||||
fi
|
||||
"${IPTABLES[@]}" -A "${inactive}" \
|
||||
-i awg0 -o awg0 -s "${source}" -d "${destination}" \
|
||||
-m comment --comment amneziawg-group-pair -j ACCEPT
|
||||
done < "${POLICY_FILE}"
|
||||
fi
|
||||
|
||||
# This DROP is deliberately restricted to packets entering and leaving
|
||||
# awg0. It cannot match Kubernetes, CNI, kubelet, Tailscale, or host traffic.
|
||||
"${IPTABLES[@]}" -A "${inactive}" \
|
||||
-i awg0 -o awg0 -s "${VPN_CIDR}" -d "${VPN_CIDR}" \
|
||||
-m comment --comment amneziawg-client-isolation -j DROP
|
||||
"${IPTABLES[@]}" -A "${inactive}" \
|
||||
-i awg0 -o "${EXT_IF}" -s "${VPN_CIDR}" \
|
||||
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
|
||||
-m comment --comment amneziawg-internet-out -j ACCEPT
|
||||
"${IPTABLES[@]}" -A "${inactive}" \
|
||||
-i "${EXT_IF}" -o awg0 -d "${VPN_CIDR}" \
|
||||
-m conntrack --ctstate ESTABLISHED,RELATED \
|
||||
-m comment --comment amneziawg-internet-return -j ACCEPT
|
||||
"${IPTABLES[@]}" -A "${inactive}" -j RETURN
|
||||
|
||||
# Insert the complete new policy before removing the old jump. There is
|
||||
# never a window with a partially built or absent active policy.
|
||||
"${IPTABLES[@]}" -I "${FORWARD_CHAIN}" 1 -j "${inactive}"
|
||||
if [ -n "${active}" ]; then
|
||||
"${IPTABLES[@]}" -D "${FORWARD_CHAIN}" -j "${active}"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -70,14 +142,40 @@ data:
|
||||
fi
|
||||
|
||||
sysctl -w net.ipv4.ip_forward=1
|
||||
sysctl -w net.ipv4.conf.awg0.send_redirects=0
|
||||
|
||||
# Remove rules written by older revisions. In particular, the unqualified
|
||||
# tailscale UDP DROP also blocks Flannel VXLAN (UDP/8472).
|
||||
delete_rule INPUT -i tailscale0 -p udp -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
ensure_insert_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
ensure_insert_rule INPUT -i tailscale0 -p udp --dport "${PORT}" -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
ensure_append_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
ensure_append_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
ensure_append_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
ensure_append_rule -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i tailscale0 -p udp --dport "${PORT}" -m comment --comment amneziawg-block-tailscale -j DROP
|
||||
delete_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
delete_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
delete_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
delete_rule INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT
|
||||
"${IPTABLES[@]}" -F AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
|
||||
ensure_chain "${FORWARD_CHAIN}"
|
||||
ensure_chain "${POLICY_CHAIN_A}"
|
||||
ensure_chain "${POLICY_CHAIN_B}"
|
||||
if ! "${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null | grep -q -- '-j RETURN'; then
|
||||
"${IPTABLES[@]}" -A "${FORWARD_CHAIN}" -j RETURN
|
||||
fi
|
||||
apply_policy
|
||||
delete_rule "${FORWARD_CHAIN}" -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
delete_rule "${FORWARD_CHAIN}" -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
|
||||
# The jump is first, but non-awg traffic immediately RETURNs to the original
|
||||
# host FORWARD chain without an ACCEPT or DROP decision.
|
||||
delete_rule FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
|
||||
"${IPTABLES[@]}" -I FORWARD 1 \
|
||||
-m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
|
||||
|
||||
ensure_rule -A -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
|
||||
/scripts/firewall-check.sh "${PORT}" "${VPN_CIDR}"
|
||||
|
||||
firewall-down.sh: |
|
||||
#!/usr/bin/env bash
|
||||
@@ -85,6 +183,10 @@ data:
|
||||
|
||||
PORT="${1:-5847}"
|
||||
VPN_CIDR="${2:-10.8.0.0/16}"
|
||||
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
|
||||
POLICY_CHAIN_A="AMNEZIAWG-POLICY-A"
|
||||
POLICY_CHAIN_B="AMNEZIAWG-POLICY-B"
|
||||
IPTABLES=(iptables -w 30)
|
||||
|
||||
external_interface() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
@@ -100,7 +202,7 @@ data:
|
||||
local chain="$1"
|
||||
shift
|
||||
|
||||
while iptables "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
while "${IPTABLES[@]}" "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do
|
||||
true
|
||||
done
|
||||
}
|
||||
@@ -111,6 +213,7 @@ data:
|
||||
fi
|
||||
|
||||
if [ -n "${EXT_IF}" ]; then
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule INPUT -i "${EXT_IF}" -p udp --dport "${PORT}" -m comment --comment amneziawg-allow-external -j ACCEPT
|
||||
delete_rule -t nat POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
fi
|
||||
@@ -120,6 +223,74 @@ data:
|
||||
delete_rule INPUT -i awg0 -m comment --comment amneziawg-awg-input -j ACCEPT
|
||||
delete_rule FORWARD -i awg0 -m comment --comment amneziawg-forward-in -j ACCEPT
|
||||
delete_rule FORWARD -o awg0 -m comment --comment amneziawg-forward-out -j ACCEPT
|
||||
delete_rule INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT
|
||||
delete_rule FORWARD -m comment --comment amneziawg-forward-jump -j "${FORWARD_CHAIN}"
|
||||
|
||||
"${IPTABLES[@]}" -F AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X AMNEZIAWG-INPUT >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -F "${FORWARD_CHAIN}" >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -F "${POLICY_CHAIN_A}" >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -F "${POLICY_CHAIN_B}" >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X "${POLICY_CHAIN_A}" >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X "${POLICY_CHAIN_B}" >/dev/null 2>&1 || true
|
||||
"${IPTABLES[@]}" -X "${FORWARD_CHAIN}" >/dev/null 2>&1 || true
|
||||
|
||||
firewall-check.sh: |
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${1:-5847}"
|
||||
VPN_CIDR="${2:-10.8.0.0/16}"
|
||||
FORWARD_CHAIN="AMNEZIAWG-FORWARD"
|
||||
IPTABLES=(iptables -w 5)
|
||||
|
||||
external_interface() {
|
||||
ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}'
|
||||
}
|
||||
|
||||
EXT_IF="$(external_interface || true)"
|
||||
if [ -z "${EXT_IF}" ]; then
|
||||
EXT_IF="$(ip route show default | awk '{print $5; exit}')"
|
||||
fi
|
||||
if [ -z "${EXT_IF}" ]; then
|
||||
echo "Unable to detect external interface" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if "${IPTABLES[@]}" -C INPUT -i tailscale0 -p udp -m comment --comment amneziawg-block-tailscale -j DROP >/dev/null 2>&1; then
|
||||
echo "Unsafe broad UDP DROP on tailscale0 is present" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "${IPTABLES[@]}" -C INPUT -i "${EXT_IF}" -p udp -m comment --comment amneziawg-allow-external -j ACCEPT >/dev/null 2>&1; then
|
||||
echo "Unsafe broad UDP ACCEPT on ${EXT_IF} is present" >&2
|
||||
exit 1
|
||||
fi
|
||||
if "${IPTABLES[@]}" -C INPUT -m comment --comment amneziawg-input-jump -j AMNEZIAWG-INPUT >/dev/null 2>&1; then
|
||||
echo "Unexpected AmneziaWG INPUT chain is present" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FIRST_FORWARD_RULE="$("${IPTABLES[@]}" -S FORWARD | grep '^-A FORWARD ' | sed -n '1p')"
|
||||
[[ "${FIRST_FORWARD_RULE}" == *"--comment amneziawg-forward-jump"* ]]
|
||||
[[ "${FIRST_FORWARD_RULE}" == *"-j ${FORWARD_CHAIN}"* ]]
|
||||
ACTIVE_POLICY="$("${IPTABLES[@]}" -S "${FORWARD_CHAIN}" 2>/dev/null \
|
||||
| awk '$1 == "-A" && $3 == "-j" && $4 ~ /^AMNEZIAWG-POLICY-[AB]$/ { print $4; exit }')"
|
||||
[ -n "${ACTIVE_POLICY}" ]
|
||||
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
|
||||
-i awg0 -o awg0 -s "${VPN_CIDR}" -d "${VPN_CIDR}" \
|
||||
-m comment --comment amneziawg-client-isolation -j DROP
|
||||
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
|
||||
-i awg0 -o "${EXT_IF}" -s "${VPN_CIDR}" \
|
||||
-m conntrack --ctstate NEW,ESTABLISHED,RELATED \
|
||||
-m comment --comment amneziawg-internet-out -j ACCEPT
|
||||
"${IPTABLES[@]}" -C "${ACTIVE_POLICY}" \
|
||||
-i "${EXT_IF}" -o awg0 -d "${VPN_CIDR}" \
|
||||
-m conntrack --ctstate ESTABLISHED,RELATED \
|
||||
-m comment --comment amneziawg-internet-return -j ACCEPT
|
||||
|
||||
"${IPTABLES[@]}" -t nat -C POSTROUTING -s "${VPN_CIDR}" -o "${EXT_IF}" -m comment --comment amneziawg-masquerade -j MASQUERADE
|
||||
[ "$(sysctl -n net.ipv4.conf.awg0.send_redirects)" = "0" ]
|
||||
awg show awg0 >/dev/null 2>&1
|
||||
|
||||
run.sh: |
|
||||
#!/usr/bin/env bash
|
||||
@@ -127,6 +298,8 @@ data:
|
||||
|
||||
SERVER_CONFIG="/etc/amnezia/server/awg0.conf"
|
||||
CLIENTS_DIR="${AMNEZIAWG_CLIENTS_DIR:-/run/amnezia/clients}"
|
||||
POLICY_FILE="${AMNEZIAWG_POLICY_FILE:-/run/amnezia/policy/policy.conf}"
|
||||
CONFIG_GENERATION="${AMNEZIAWG_CONFIG_GENERATION:-/run/amnezia/config-generation}"
|
||||
RUNTIME_CONFIG="/run/amnezia/awg0.conf"
|
||||
SYNC_CONFIG="/run/amnezia/awg0.sync.conf"
|
||||
STATUS_FILE="/run/amnezia/reload-status"
|
||||
@@ -161,13 +334,12 @@ data:
|
||||
chmod 0600 "${RUNTIME_CONFIG}"
|
||||
}
|
||||
|
||||
client_config_hash() {
|
||||
{
|
||||
for client_config in "${CLIENTS_DIR}"/*; do
|
||||
[ -f "${client_config}" ] || continue
|
||||
sha256sum "${client_config}"
|
||||
done
|
||||
} | sha256sum | awk '{print $1}'
|
||||
runtime_config_hash() {
|
||||
if [ -f "${CONFIG_GENERATION}" ]; then
|
||||
sha256sum "${CONFIG_GENERATION}" | awk '{print $1}'
|
||||
else
|
||||
printf 'missing\n'
|
||||
fi
|
||||
}
|
||||
|
||||
write_reload_status() {
|
||||
@@ -188,10 +360,14 @@ data:
|
||||
}
|
||||
|
||||
apply_live_config() {
|
||||
# Fail closed during a peer/key and policy transition. /dev/null produces
|
||||
# an Internet-only policy with all awg0-to-awg0 traffic isolated.
|
||||
/scripts/firewall-up.sh 5847 10.8.0.0/16 /dev/null
|
||||
render_config
|
||||
awg-quick strip "${RUNTIME_CONFIG}" > "${SYNC_CONFIG}"
|
||||
chmod 0600 "${SYNC_CONFIG}"
|
||||
awg syncconf awg0 "${SYNC_CONFIG}"
|
||||
/scripts/firewall-up.sh 5847 10.8.0.0/16 "${POLICY_FILE}"
|
||||
}
|
||||
|
||||
watch_client_config() {
|
||||
@@ -201,7 +377,7 @@ data:
|
||||
wait "$!" || return 0
|
||||
|
||||
local current_hash
|
||||
current_hash="$(client_config_hash)"
|
||||
current_hash="$(runtime_config_hash)"
|
||||
if [ "${current_hash}" = "${last_hash}" ]; then
|
||||
continue
|
||||
fi
|
||||
@@ -221,7 +397,7 @@ data:
|
||||
trap cleanup EXIT
|
||||
trap 'exit 0' TERM INT
|
||||
|
||||
initial_hash="$(client_config_hash)"
|
||||
initial_hash="$(runtime_config_hash)"
|
||||
render_config
|
||||
cleanup
|
||||
awg-quick up "${RUNTIME_CONFIG}"
|
||||
@@ -237,6 +413,8 @@ data:
|
||||
CLIENT_SECRET_KEY="${AMNEZIAWG_CLIENT_SECRET_KEY:-peers.conf}"
|
||||
CLIENTS_DIR="${AMNEZIAWG_CLIENTS_DIR:-/run/amnezia/clients}"
|
||||
PEERS_FILE="${CLIENTS_DIR}/peers.conf"
|
||||
POLICY_FILE="${AMNEZIAWG_POLICY_FILE:-/run/amnezia/policy/policy.conf}"
|
||||
CONFIG_GENERATION="${AMNEZIAWG_CONFIG_GENERATION:-/run/amnezia/config-generation}"
|
||||
SYNC_INTERVAL="${AMNEZIAWG_CLIENT_SECRET_SYNC_INTERVAL:-5}"
|
||||
NAMESPACE="${POD_NAMESPACE:-$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)}"
|
||||
|
||||
@@ -246,33 +424,66 @@ data:
|
||||
: > "${PEERS_FILE}"
|
||||
chmod 0600 "${PEERS_FILE}"
|
||||
fi
|
||||
mkdir -p "$(dirname "${POLICY_FILE}")"
|
||||
if [ ! -f "${POLICY_FILE}" ]; then
|
||||
: > "${POLICY_FILE}"
|
||||
chmod 0600 "${POLICY_FILE}"
|
||||
fi
|
||||
{
|
||||
sha256sum "${PEERS_FILE}"
|
||||
sha256sum "${POLICY_FILE}"
|
||||
} | sha256sum | awk '{print $1}' > "${CONFIG_GENERATION}"
|
||||
}
|
||||
|
||||
sync_once() {
|
||||
mkdir -p "${CLIENTS_DIR}"
|
||||
local tmp_file="${PEERS_FILE}.tmp"
|
||||
local encoded=""
|
||||
mkdir -p "$(dirname "${POLICY_FILE}")"
|
||||
local snapshot="${CONFIG_GENERATION}.snapshot"
|
||||
local peers_tmp="${PEERS_FILE}.tmp"
|
||||
local policy_tmp="${POLICY_FILE}.tmp"
|
||||
local peers_encoded policy_encoded generation
|
||||
|
||||
if ! encoded="$(kubectl get secret "${CLIENT_SECRET}" -n "${NAMESPACE}" -o "go-template={{ index .data \"${CLIENT_SECRET_KEY}\" }}" 2>/dev/null)"; then
|
||||
if ! kubectl get secret "${CLIENT_SECRET}" -n "${NAMESPACE}" \
|
||||
-o "go-template={{ with index .data \"${CLIENT_SECRET_KEY}\" }}{{ . }}{{ end }}{{ \"\n\" }}{{ with index .data \"policy.conf\" }}{{ . }}{{ end }}{{ \"\n\" }}" \
|
||||
> "${snapshot}" 2>/dev/null; then
|
||||
echo "WARN: failed to read Secret ${NAMESPACE}/${CLIENT_SECRET}; keeping current peers" >&2
|
||||
rm -f "${snapshot}"
|
||||
write_empty_once
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -n "${encoded}" ]; then
|
||||
printf '%s' "${encoded}" | base64 -d > "${tmp_file}"
|
||||
else
|
||||
: > "${tmp_file}"
|
||||
fi
|
||||
chmod 0600 "${tmp_file}"
|
||||
peers_encoded="$(sed -n '1p' "${snapshot}")"
|
||||
policy_encoded="$(sed -n '2p' "${snapshot}")"
|
||||
rm -f "${snapshot}"
|
||||
|
||||
if [ -f "${PEERS_FILE}" ] && cmp -s "${tmp_file}" "${PEERS_FILE}"; then
|
||||
rm -f "${tmp_file}"
|
||||
if [ -n "${peers_encoded}" ]; then
|
||||
printf '%s' "${peers_encoded}" | base64 -d > "${peers_tmp}"
|
||||
else
|
||||
: > "${peers_tmp}"
|
||||
fi
|
||||
if [ -n "${policy_encoded}" ]; then
|
||||
printf '%s' "${policy_encoded}" | base64 -d > "${policy_tmp}"
|
||||
else
|
||||
# Missing policy is fail-closed: no client-to-client pairs are allowed.
|
||||
: > "${policy_tmp}"
|
||||
fi
|
||||
chmod 0600 "${peers_tmp}" "${policy_tmp}"
|
||||
|
||||
generation="$({
|
||||
sha256sum "${peers_tmp}"
|
||||
sha256sum "${policy_tmp}"
|
||||
} | sha256sum | awk '{print $1}')"
|
||||
if [ -f "${CONFIG_GENERATION}" ] \
|
||||
&& [ "$(sed -n '1p' "${CONFIG_GENERATION}")" = "${generation}" ]; then
|
||||
rm -f "${peers_tmp}" "${policy_tmp}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
mv "${tmp_file}" "${PEERS_FILE}"
|
||||
echo "Synced AmneziaWG client peers from Secret ${NAMESPACE}/${CLIENT_SECRET}:${CLIENT_SECRET_KEY}"
|
||||
mv "${policy_tmp}" "${POLICY_FILE}"
|
||||
mv "${peers_tmp}" "${PEERS_FILE}"
|
||||
printf '%s\n' "${generation}" > "${CONFIG_GENERATION}.tmp"
|
||||
mv "${CONFIG_GENERATION}.tmp" "${CONFIG_GENERATION}"
|
||||
echo "Synced AmneziaWG peers and policy from Secret ${NAMESPACE}/${CLIENT_SECRET}"
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "once" ]; then
|
||||
|
||||
@@ -123,20 +123,29 @@ spec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- awg show awg0 >/dev/null 2>&1
|
||||
- /scripts/firewall-check.sh 5847 10.8.0.0/16
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
timeoutSeconds: 3
|
||||
failureThreshold: 3
|
||||
timeoutSeconds: 8
|
||||
failureThreshold: 2
|
||||
startupProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
|
||||
periodSeconds: 5
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 12
|
||||
livenessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/bash
|
||||
- -lc
|
||||
- awg show awg0 >/dev/null 2>&1
|
||||
- /scripts/firewall-check.sh 5847 10.8.0.0/16 || /scripts/firewall-up.sh 5847 10.8.0.0/16
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
timeoutSeconds: 15
|
||||
failureThreshold: 3
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: firefly-iii
|
||||
namespace: argocd
|
||||
spec:
|
||||
project: apps
|
||||
destination:
|
||||
namespace: firefly-iii
|
||||
server: https://kubernetes.default.svc
|
||||
source:
|
||||
repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git
|
||||
targetRevision: HEAD
|
||||
path: k8s/apps/firefly-iii
|
||||
syncPolicy:
|
||||
automated:
|
||||
selfHeal: true
|
||||
prune: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: firefly-data-importer-auth
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: http://auth-proxy.auth-proxy.svc:80/auth
|
||||
trustForwardHeader: true
|
||||
authResponseHeaders:
|
||||
- X-Auth-Request-User
|
||||
- X-Auth-Request-Email
|
||||
- X-Auth-Request-Groups
|
||||
---
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: IngressRoute
|
||||
metadata:
|
||||
name: firefly-data-importer
|
||||
spec:
|
||||
entryPoints:
|
||||
- websecure
|
||||
routes:
|
||||
# OAuth callback is intentionally public. Data Importer validates both the
|
||||
# state parameter and its browser session before exchanging the code.
|
||||
- match: Host(`import.hexor.cy`) && Path(`/eb-callback`)
|
||||
kind: Rule
|
||||
priority: 200
|
||||
services:
|
||||
- name: firefly-data-importer
|
||||
port: 80
|
||||
# Everything else requires a valid auth-proxy session and group.
|
||||
- match: Host(`import.hexor.cy`)
|
||||
kind: Rule
|
||||
priority: 100
|
||||
middlewares:
|
||||
- name: firefly-data-importer-auth
|
||||
services:
|
||||
- name: firefly-data-importer
|
||||
port: 80
|
||||
tls:
|
||||
secretName: firefly-data-importer-tls
|
||||
---
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: firefly-data-importer-tls
|
||||
spec:
|
||||
secretName: firefly-data-importer-tls
|
||||
issuerRef:
|
||||
name: letsencrypt
|
||||
kind: ClusterIssuer
|
||||
dnsNames:
|
||||
- import.hexor.cy
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: firefly-data-importer
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
annotations:
|
||||
reloader.stakater.com/auto: "true"
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
spec:
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: data-importer
|
||||
image: fireflyiii/data-importer:version-2.3.4
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
- name: FIREFLY_III_URL
|
||||
value: http://firefly-iii
|
||||
- name: VANITY_URL
|
||||
value: http://ff.lan
|
||||
- name: TRUSTED_PROXIES
|
||||
value: "**"
|
||||
- name: EXPECT_SECURE_URL
|
||||
value: "false"
|
||||
- name: VERIFY_TLS_SECURITY
|
||||
value: "true"
|
||||
- name: TZ
|
||||
value: Europe/London
|
||||
- name: APP_ENV
|
||||
value: production
|
||||
- name: APP_DEBUG
|
||||
value: "false"
|
||||
- name: LOG_LEVEL
|
||||
value: notice
|
||||
- name: ENABLE_BANKING_APP_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: firefly-data-importer-secrets
|
||||
key: ENABLE_BANKING_APP_ID
|
||||
- name: ENABLE_BANKING_PRIVATE_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: firefly-data-importer-secrets
|
||||
key: ENABLE_BANKING_PRIVATE_KEY
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: http
|
||||
initialDelaySeconds: 20
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 128Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 512Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: firefly-data-importer
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
ports:
|
||||
- name: http
|
||||
port: 80
|
||||
targetPort: http
|
||||
protocol: TCP
|
||||
@@ -0,0 +1,85 @@
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: firefly-secrets
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
target:
|
||||
name: firefly-secrets
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Delete
|
||||
template:
|
||||
type: Opaque
|
||||
data:
|
||||
# Firefly and CloudNativePG deliberately share this generated Secret.
|
||||
username: firefly
|
||||
password: |-
|
||||
{{ .db_password }}
|
||||
DB_PASSWORD: |-
|
||||
{{ .db_password }}
|
||||
APP_KEY: |-
|
||||
{{ .app_key }}
|
||||
STATIC_CRON_TOKEN: |-
|
||||
{{ .cron_token }}
|
||||
data:
|
||||
- secretKey: db_password
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[0].value
|
||||
- secretKey: app_key
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[1].value
|
||||
- secretKey: cron_token
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[2].value
|
||||
---
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: firefly-data-importer-secrets
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
target:
|
||||
name: firefly-data-importer-secrets
|
||||
creationPolicy: Owner
|
||||
deletionPolicy: Delete
|
||||
template:
|
||||
type: Opaque
|
||||
data:
|
||||
ENABLE_BANKING_APP_ID: |-
|
||||
{{ .app_id }}
|
||||
ENABLE_BANKING_PRIVATE_KEY: |-
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
{{ .private_key | replace "-----BEGIN PRIVATE KEY-----" "" | replace "-----END PRIVATE KEY-----" "" | replace " " "" | trim }}
|
||||
-----END PRIVATE KEY-----
|
||||
data:
|
||||
- secretKey: app_id
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[3].value
|
||||
- secretKey: private_key
|
||||
sourceRef:
|
||||
storeRef:
|
||||
name: vaultwarden-login
|
||||
kind: ClusterSecretStore
|
||||
remoteRef:
|
||||
key: a1867c81-715c-47cd-978d-14ea5bcedea9
|
||||
property: fields[4].value
|
||||
@@ -0,0 +1,101 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources:
|
||||
- app.yaml
|
||||
- data-importer.yaml
|
||||
- data-importer-ingress.yaml
|
||||
- external-secrets.yaml
|
||||
- postgres.yaml
|
||||
- storage.yaml
|
||||
- storage-prep.yaml
|
||||
- traefik-ai.yaml
|
||||
- network-policy.yaml
|
||||
|
||||
helmCharts:
|
||||
- name: firefly-iii
|
||||
repo: https://harish2k01.github.io/helm-charts
|
||||
version: 0.1.2
|
||||
releaseName: firefly-iii
|
||||
namespace: firefly-iii
|
||||
valuesFile: values.yaml
|
||||
includeCRDs: true
|
||||
|
||||
patches:
|
||||
# Chart 0.1.2 requires postgres.enabled=true. Remove its StatefulSet and
|
||||
# make the generated database Service select the CloudNativePG instance.
|
||||
- target:
|
||||
group: apps
|
||||
version: v1
|
||||
kind: StatefulSet
|
||||
name: firefly-iii-postgres
|
||||
patch: |-
|
||||
$patch: delete
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: firefly-iii-postgres
|
||||
- target:
|
||||
version: v1
|
||||
kind: Service
|
||||
name: firefly-iii-postgres
|
||||
patch: |-
|
||||
- op: replace
|
||||
path: /spec/selector
|
||||
value:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
# The upstream chart does not expose scheduling settings for its CronJob.
|
||||
- target:
|
||||
group: batch
|
||||
version: v1
|
||||
kind: CronJob
|
||||
name: firefly-iii-cron
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /spec/concurrencyPolicy
|
||||
value: Forbid
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/backoffLimit
|
||||
value: 1
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/activeDeadlineSeconds
|
||||
value: 600
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/template/spec/nodeSelector
|
||||
value:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
- op: add
|
||||
path: /spec/jobTemplate/spec/template/spec/tolerations
|
||||
value:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
- op: replace
|
||||
path: /spec/jobTemplate/spec/template/spec/containers/0/command
|
||||
value:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- >-
|
||||
curl --fail --silent --show-error
|
||||
--retry 30 --retry-delay 10 --retry-all-errors
|
||||
--connect-timeout 5 --max-time 30
|
||||
"http://firefly-iii:80/api/v1/cron/${STATIC_CRON_TOKEN}"
|
||||
# A second independent filter in addition to ingressClassName.
|
||||
- target:
|
||||
group: networking.k8s.io
|
||||
version: v1
|
||||
kind: Ingress
|
||||
name: firefly-iii
|
||||
patch: |-
|
||||
- op: add
|
||||
path: /metadata/labels/firefly.hexor.cy~1private-ai
|
||||
value: "true"
|
||||
# The shared Traefik has the same controller identifier and can discover
|
||||
# IngressClass objects. The explicit annotation partitions this Ingress
|
||||
# so only the instance configured with ingressclass=traefik-ai accepts it.
|
||||
- op: add
|
||||
path: /metadata/annotations/kubernetes.io~1ingress.class
|
||||
value: traefik-ai
|
||||
- op: remove
|
||||
path: /spec/ingressClassName
|
||||
@@ -0,0 +1,98 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
# The chart's CronJob uses the same selector labels as the application.
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
# hostNetwork traffic is seen as node traffic rather than Pod traffic.
|
||||
- ipBlock:
|
||||
cidr: 192.168.1.117/32
|
||||
- ipBlock:
|
||||
cidr: 100.77.155.120/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-postgres-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/instance: firefly-iii
|
||||
app.kubernetes.io/name: firefly-iii
|
||||
- podSelector:
|
||||
matchLabels:
|
||||
cnpg.io/cluster: firefly-postgres
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 5432
|
||||
# CloudNativePG operator health/status traffic to the instance manager.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: psql
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: cloudnative-pg
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8000
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: firefly-data-importer-ingress
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-data-importer
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
# Shared Traefik serves the public HTTPS importer.
|
||||
- from:
|
||||
- namespaceSelector:
|
||||
matchLabels:
|
||||
kubernetes.io/metadata.name: kube-system
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
# Keep node traffic allowed for probes and local diagnostics.
|
||||
- from:
|
||||
- ipBlock:
|
||||
cidr: 192.168.1.117/32
|
||||
- ipBlock:
|
||||
cidr: 100.77.155.120/32
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
@@ -0,0 +1,49 @@
|
||||
apiVersion: postgresql.cnpg.io/v1
|
||||
kind: Cluster
|
||||
metadata:
|
||||
name: firefly-postgres
|
||||
spec:
|
||||
description: PostgreSQL for Firefly III
|
||||
instances: 1
|
||||
enableSuperuserAccess: false
|
||||
primaryUpdateStrategy: unsupervised
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: firefly
|
||||
owner: firefly
|
||||
secret:
|
||||
name: firefly-secrets
|
||||
dataChecksums: true
|
||||
storage:
|
||||
size: 10Gi
|
||||
storageClass: firefly-local
|
||||
pvcTemplate:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: firefly-local
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: firefly-postgres
|
||||
resources:
|
||||
requests:
|
||||
storage: 10Gi
|
||||
volumeMode: Filesystem
|
||||
affinity:
|
||||
enablePodAntiAffinity: false
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "1"
|
||||
memory: 1Gi
|
||||
monitoring:
|
||||
enablePodMonitor: true
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: firefly-storage-prep
|
||||
annotations:
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
|
||||
spec:
|
||||
backoffLimit: 3
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-storage-prep
|
||||
spec:
|
||||
restartPolicy: OnFailure
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: prepare
|
||||
image: busybox:1.37.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
mkdir -p /host/k8s/firefly-iii/postgres
|
||||
chown 26:26 /host/k8s/firefly-iii/postgres
|
||||
chmod 0700 /host/k8s/firefly-iii/postgres
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- CHOWN
|
||||
- DAC_OVERRIDE
|
||||
- FOWNER
|
||||
volumeMounts:
|
||||
- name: host-root
|
||||
mountPath: /host/k8s/firefly-iii
|
||||
volumes:
|
||||
- name: host-root
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii
|
||||
type: DirectoryOrCreate
|
||||
@@ -0,0 +1,72 @@
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: firefly-local
|
||||
provisioner: kubernetes.io/no-provisioner
|
||||
reclaimPolicy: Retain
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: firefly-postgres
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-postgres
|
||||
spec:
|
||||
capacity:
|
||||
storage: 10Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: firefly-local
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii/postgres
|
||||
type: DirectoryOrCreate
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- ai.tail2fe2d.ts.net
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolume
|
||||
metadata:
|
||||
name: firefly-uploads
|
||||
labels:
|
||||
app.kubernetes.io/name: firefly-uploads
|
||||
spec:
|
||||
capacity:
|
||||
storage: 5Gi
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
persistentVolumeReclaimPolicy: Retain
|
||||
storageClassName: firefly-local
|
||||
volumeMode: Filesystem
|
||||
hostPath:
|
||||
path: /k8s/firefly-iii/uploads
|
||||
type: DirectoryOrCreate
|
||||
nodeAffinity:
|
||||
required:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/hostname
|
||||
operator: In
|
||||
values:
|
||||
- ai.tail2fe2d.ts.net
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: firefly-uploads
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: firefly-local
|
||||
volumeName: firefly-uploads
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
@@ -0,0 +1,175 @@
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: IngressClass
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
spec:
|
||||
controller: traefik.io/ingress-controller
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- services
|
||||
- secrets
|
||||
- endpoints
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- discovery.k8s.io
|
||||
resources:
|
||||
- endpointslices
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingresses/status
|
||||
verbs:
|
||||
- update
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: traefik-ai
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: traefik-ai
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: firefly-traefik-ai
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- ingressclasses
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: firefly-traefik-ai
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: firefly-traefik-ai
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: traefik-ai
|
||||
namespace: firefly-iii
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: traefik-ai
|
||||
labels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
spec:
|
||||
replicas: 1
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: traefik-ai
|
||||
spec:
|
||||
serviceAccountName: traefik-ai
|
||||
hostNetwork: true
|
||||
dnsPolicy: ClusterFirstWithHostNet
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: traefik
|
||||
image: rancher/mirrored-library-traefik:3.6.13
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --entrypoints.web.address=192.168.1.117:80
|
||||
- --providers.kubernetesingress=true
|
||||
- --providers.kubernetesingress.namespaces=firefly-iii
|
||||
- --providers.kubernetesingress.ingressclass=traefik-ai
|
||||
- --providers.kubernetesingress.labelselector=firefly.hexor.cy/private-ai=true
|
||||
- --providers.kubernetescrd=false
|
||||
- --api.dashboard=false
|
||||
- --log.level=INFO
|
||||
ports:
|
||||
- name: web
|
||||
containerPort: 80
|
||||
hostPort: 80
|
||||
hostIP: 192.168.1.117
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
tcpSocket:
|
||||
host: 192.168.1.117
|
||||
port: web
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
host: 192.168.1.117
|
||||
port: web
|
||||
periodSeconds: 30
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 256Mi
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities:
|
||||
drop:
|
||||
- ALL
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
readOnlyRootFilesystem: true
|
||||
runAsNonRoot: false
|
||||
runAsUser: 0
|
||||
runAsGroup: 0
|
||||
securityContext:
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
@@ -0,0 +1,68 @@
|
||||
replicaCount: 1
|
||||
|
||||
image:
|
||||
repository: fireflyiii/core
|
||||
tag: version-6.6.1
|
||||
pullPolicy: IfNotPresent
|
||||
|
||||
service:
|
||||
type: ClusterIP
|
||||
port: 80
|
||||
targetPort: 8080
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik-ai
|
||||
hosts:
|
||||
- host: ff.lan
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
|
||||
persistence:
|
||||
upload:
|
||||
enabled: true
|
||||
existingClaim: firefly-uploads
|
||||
|
||||
# Must remain true because chart 0.1.2 validates it. Kustomize removes the
|
||||
# generated StatefulSet and makes this Service select the CloudNativePG Pod.
|
||||
postgres:
|
||||
enabled: true
|
||||
auth:
|
||||
database: firefly
|
||||
username: firefly
|
||||
service:
|
||||
port: 5432
|
||||
|
||||
cronjob:
|
||||
enabled: true
|
||||
schedule: "0 3 * * *"
|
||||
timeZone: Europe/London
|
||||
|
||||
firefly:
|
||||
env:
|
||||
APP_ENV: production
|
||||
APP_DEBUG: "false"
|
||||
APP_URL: http://ff.lan
|
||||
SITE_OWNER: owner@ff.lan
|
||||
TZ: Europe/London
|
||||
DEFAULT_LANGUAGE: en_US
|
||||
TRUSTED_PROXIES: 10.42.0.0/16
|
||||
COOKIE_SECURE: "false"
|
||||
PGSQL_SSL_MODE: prefer
|
||||
secrets:
|
||||
existingSecret: firefly-secrets
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: ai.tail2fe2d.ts.net
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: ai
|
||||
effect: NoSchedule
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: "2"
|
||||
memory: 1Gi
|
||||
@@ -79,6 +79,7 @@ spec:
|
||||
labels:
|
||||
app: gitea-runner
|
||||
spec:
|
||||
serviceAccountName: gitea-runner
|
||||
dnsConfig:
|
||||
options:
|
||||
- name: ndots
|
||||
|
||||
@@ -5,6 +5,8 @@ resources:
|
||||
- app.yaml
|
||||
- external-secrets.yaml
|
||||
- deployment.yaml
|
||||
- runner-rbac.yaml
|
||||
- runner-rebalance.yaml
|
||||
- user-unban-cronjob.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: gitea-runner
|
||||
namespace: gitea
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: gitea-runner-kubernetes-token
|
||||
namespace: gitea
|
||||
annotations:
|
||||
kubernetes.io/service-account.name: gitea-runner
|
||||
type: kubernetes.io/service-account-token
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: auth-proxy-routes-manager
|
||||
namespace: auth-proxy
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
resourceNames:
|
||||
- auth-proxy-routes
|
||||
verbs:
|
||||
- get
|
||||
- update
|
||||
- patch
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
verbs:
|
||||
- create
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: gitea-runner-auth-proxy-routes
|
||||
namespace: auth-proxy
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: auth-proxy-routes-manager
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: gitea-runner
|
||||
namespace: gitea
|
||||
@@ -0,0 +1,127 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- pods
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- delete
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: gitea-runner-rebalancer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- nodes
|
||||
resourceNames:
|
||||
- ai.tail2fe2d.ts.net
|
||||
verbs:
|
||||
- get
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: gitea-runner-rebalancer
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: CronJob
|
||||
metadata:
|
||||
name: gitea-runner-rebalancer
|
||||
namespace: gitea
|
||||
spec:
|
||||
schedule: "*/2 * * * *"
|
||||
concurrencyPolicy: Forbid
|
||||
successfulJobsHistoryLimit: 1
|
||||
failedJobsHistoryLimit: 3
|
||||
jobTemplate:
|
||||
spec:
|
||||
ttlSecondsAfterFinished: 300
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: gitea-runner-rebalancer
|
||||
spec:
|
||||
serviceAccountName: gitea-runner-rebalancer
|
||||
restartPolicy: Never
|
||||
containers:
|
||||
- name: rebalance
|
||||
image: bitnami/kubectl:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
command:
|
||||
- /bin/sh
|
||||
- -ec
|
||||
- |
|
||||
target_node="ai.tail2fe2d.ts.net"
|
||||
ready="$(
|
||||
kubectl get node "${target_node}" \
|
||||
-o jsonpath='{range .status.conditions[?(@.type=="Ready")]}{.status}{end}'
|
||||
)"
|
||||
|
||||
if [ "${ready}" != "True" ]; then
|
||||
echo "${target_node} is not Ready; keeping the runner on its current node"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
runner_pod="$(
|
||||
kubectl -n gitea get pods \
|
||||
-l app=gitea-runner \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}'
|
||||
)"
|
||||
runner_node="$(
|
||||
kubectl -n gitea get pods \
|
||||
-l app=gitea-runner \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].spec.nodeName}'
|
||||
)"
|
||||
|
||||
if [ -z "${runner_pod}" ] || [ -z "${runner_node}" ]; then
|
||||
echo "No running Gitea runner found"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${runner_node}" = "${target_node}" ]; then
|
||||
echo "${runner_pod} already runs on ${target_node}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Moving ${runner_pod} from ${runner_node} to preferred node ${target_node}"
|
||||
kubectl -n gitea delete pod "${runner_pod}" --wait=false
|
||||
@@ -10,7 +10,7 @@ resources:
|
||||
helmCharts:
|
||||
- name: argo-cd
|
||||
repo: https://argoproj.github.io/argo-helm
|
||||
version: 9.4.10
|
||||
version: 10.2.1
|
||||
releaseName: argocd
|
||||
namespace: argocd
|
||||
valuesFile: values.yaml
|
||||
|
||||
Generated
-44
@@ -1,44 +0,0 @@
|
||||
# This file is maintained automatically by "terraform init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.terraform.io/goauthentik/authentik" {
|
||||
version = "2025.12.1"
|
||||
constraints = ">= 2023.10.0, 2025.12.1"
|
||||
hashes = [
|
||||
"h1:p9AGeRqK50wTHEIp7z7O4MUP83cs+lt7wPajZ9m9TB8=",
|
||||
"zh:0e856d3b13614bc32346a236a8e84ba55ecd17238c2008d4b3e71aa8cb49f515",
|
||||
"zh:2dcc44cd499c18ebbc4f763eff97a7b725763c8ac8fbb5d69c935413ccdc4962",
|
||||
"zh:434100fc75ec7cd6b64cc9497e8273e79325fa8d285e9fd9d341c1a67421643b",
|
||||
"zh:483484f66d2e8ce6fa4bfd91e824ceebf07d10acb5df5f366397c55227c4ae91",
|
||||
"zh:596743a6f1c77a6f103b06ef8d932fe8f2376793b92478853dc84571d17c429f",
|
||||
"zh:5ed2d5eb7db13229baaf042c725d5c64b58ffdcc641370175e0a88900af94bf1",
|
||||
"zh:8aecd4cf782c82bee01098f72fe4ffff83707516007b32a01c7fcb19a9260338",
|
||||
"zh:928c05ecac309287ff7d73ed6e478350fe3003557658ae5dc2be817a4268dba7",
|
||||
"zh:9b9fd36dfb3e75da8b4478485272505ae9a3c67b10db173e1d2d76cfe2b637b8",
|
||||
"zh:ab7cd8c61ab67a045854e32f0be1940a92746770dbf3c17bbe923e0259c4f897",
|
||||
"zh:bb1360ec19a4fc1095d0ef1b7b6c5c3c1a91daac7cd1957d43a4cdbb7356a2e3",
|
||||
"zh:d2186f4063aa1a547b52a53745d472e43f5343bc1674f2bbb91421c61b0fab50",
|
||||
"zh:d74bbb67a77951b18ffd7b2863954e70ac03450ad2023cc305c66a5ff25d8d18",
|
||||
"zh:f5970569ea0a479bbfbf2d452f5962e1c9bd472b82756db822d0e951363daa25",
|
||||
]
|
||||
}
|
||||
|
||||
provider "registry.terraform.io/hashicorp/random" {
|
||||
version = "3.8.1"
|
||||
constraints = ">= 3.5.0"
|
||||
hashes = [
|
||||
"h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=",
|
||||
"zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4",
|
||||
"zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae",
|
||||
"zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57",
|
||||
"zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0",
|
||||
"zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66",
|
||||
"zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511",
|
||||
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
|
||||
"zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9",
|
||||
"zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05",
|
||||
"zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8",
|
||||
"zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b",
|
||||
"zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699",
|
||||
]
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
# Authentik Terraform Configuration
|
||||
|
||||
Root Terraform configuration for managing Authentik SSO — applications (OAuth2/OIDC, Proxy, SAML), groups, outposts, flows, certificates, and property mappings.
|
||||
|
||||
State is stored in Terraform Cloud (organization `ultradesu`, workspace `Authentik`).
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── main.tf # Resources: groups, outposts, policy bindings, module calls
|
||||
├── variables.tf # Input variable definitions
|
||||
├── outputs.tf # Outputs (app details, groups, flows, wiki data)
|
||||
├── providers.tf # Authentik provider (goauthentik/authentik 2025.12.1)
|
||||
├── state.tf # Terraform Cloud backend
|
||||
├── terraform.tfvars # General settings: authentik_url, outposts, flows, tags
|
||||
├── oauth2-apps.auto.tfvars # OAuth2/OIDC application definitions
|
||||
├── proxy-apps.auto.tfvars # Proxy application definitions
|
||||
├── groups.auto.tfvars # Group definitions
|
||||
└── modules/
|
||||
├── oauth-provider/ # OAuth2/OIDC provider + application
|
||||
├── proxy-provider/ # Proxy provider + application
|
||||
└── saml-provider/ # SAML provider + application
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# Set the API token
|
||||
export TF_VAR_authentik_token="..."
|
||||
|
||||
terraform init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
All `*.auto.tfvars` files are loaded automatically — no `-var-file` flags needed.
|
||||
|
||||
## Adding applications
|
||||
|
||||
OAuth2/OIDC — add to `oauth2-apps.auto.tfvars`:
|
||||
|
||||
```hcl
|
||||
oauth_applications = {
|
||||
"my-app" = {
|
||||
name = "My App"
|
||||
slug = "my-app"
|
||||
group = "Tools"
|
||||
redirect_uris = ["https://my-app.example.com/callback"]
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Proxy — add to `proxy-apps.auto.tfvars`:
|
||||
|
||||
```hcl
|
||||
proxy_applications = {
|
||||
"my-proxy" = {
|
||||
name = "My Proxy"
|
||||
slug = "my-proxy"
|
||||
group = "Tools"
|
||||
external_host = "https://my-proxy.example.com"
|
||||
internal_host = "http://my-service.namespace.svc:80"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## CI/CD
|
||||
|
||||
Managed via Gitea Actions (`.gitea/workflows/authentik-apps.yaml`). Runs `terraform apply` on push to `main` when files in `terraform/authentik/` change. Also generates a wiki page with the applications list.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Terraform >= 1.0
|
||||
- goauthentik/authentik provider 2025.12.1
|
||||
- Authentik API token with admin permissions
|
||||
@@ -1,10 +0,0 @@
|
||||
groups = {
|
||||
"admins" = {
|
||||
name = "Administrators"
|
||||
is_superuser = true
|
||||
attributes = {
|
||||
notes = "Managed by Terraform"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,361 +0,0 @@
|
||||
data "authentik_flow" "default_authorization_flow" {
|
||||
slug = var.default_authorization_flow
|
||||
}
|
||||
|
||||
data "authentik_flow" "default_authentication_flow" {
|
||||
slug = var.default_authentication_flow
|
||||
}
|
||||
|
||||
data "authentik_flow" "default_invalidation_flow" {
|
||||
slug = var.default_invalidation_flow
|
||||
}
|
||||
|
||||
# Root groups (without parent)
|
||||
resource "authentik_group" "root_groups" {
|
||||
for_each = {
|
||||
for k, v in var.groups : k => v
|
||||
if v.parent == null
|
||||
}
|
||||
|
||||
name = each.value.name
|
||||
is_superuser = each.value.is_superuser
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
}
|
||||
|
||||
# Child groups (with parent)
|
||||
resource "authentik_group" "child_groups" {
|
||||
for_each = {
|
||||
for k, v in var.groups : k => v
|
||||
if v.parent != null
|
||||
}
|
||||
|
||||
name = each.value.name
|
||||
is_superuser = each.value.is_superuser
|
||||
parents = authentik_group.root_groups[each.value.parent].id
|
||||
attributes = jsonencode(each.value.attributes)
|
||||
|
||||
depends_on = [authentik_group.root_groups]
|
||||
}
|
||||
|
||||
# Auto-created groups for proxy applications
|
||||
resource "authentik_group" "proxy_app_groups" {
|
||||
for_each = {
|
||||
for k, v in var.proxy_applications : k => v
|
||||
if v.create_group == true
|
||||
}
|
||||
|
||||
name = "TF-${each.value.name} Users"
|
||||
is_superuser = false
|
||||
attributes = jsonencode({
|
||||
notes = "Auto-created for ${each.value.name} application"
|
||||
app_slug = each.value.slug
|
||||
})
|
||||
}
|
||||
|
||||
# Auto-created groups for OAuth applications
|
||||
resource "authentik_group" "oauth_app_groups" {
|
||||
for_each = {
|
||||
for k, v in var.oauth_applications : k => v
|
||||
if v.create_group == true
|
||||
}
|
||||
|
||||
name = "TF-${each.value.name} Users"
|
||||
is_superuser = false
|
||||
attributes = jsonencode({
|
||||
notes = "Auto-created for ${each.value.name} application"
|
||||
app_slug = each.value.slug
|
||||
})
|
||||
}
|
||||
|
||||
resource "authentik_certificate_key_pair" "certificates" {
|
||||
for_each = var.certificates
|
||||
|
||||
name = each.value.name
|
||||
certificate_data = each.value.certificate_data
|
||||
key_data = each.value.key_data
|
||||
}
|
||||
|
||||
|
||||
data "authentik_service_connection_kubernetes" "local_k8s" {
|
||||
name = "Local Kubernetes Cluster"
|
||||
}
|
||||
|
||||
resource "authentik_flow" "flows" {
|
||||
for_each = var.flows
|
||||
|
||||
name = each.value.name
|
||||
title = each.value.title
|
||||
slug = each.value.slug
|
||||
designation = each.value.designation
|
||||
policy_engine_mode = each.value.policy_engine_mode
|
||||
compatibility_mode = each.value.compatibility_mode
|
||||
layout = each.value.layout
|
||||
denied_action = each.value.denied_action
|
||||
}
|
||||
|
||||
resource "authentik_property_mapping_provider_scope" "oidc_mappings" {
|
||||
for_each = {
|
||||
for k, v in var.property_mappings : k => v
|
||||
if v.oidc_scope != null
|
||||
}
|
||||
|
||||
name = each.value.name
|
||||
scope_name = each.value.oidc_scope
|
||||
expression = each.value.expression
|
||||
}
|
||||
|
||||
resource "authentik_property_mapping_provider_saml" "saml_mappings" {
|
||||
for_each = {
|
||||
for k, v in var.property_mappings : k => v
|
||||
if v.saml_name != null
|
||||
}
|
||||
|
||||
name = each.value.name
|
||||
saml_name = each.value.saml_name
|
||||
expression = each.value.expression
|
||||
}
|
||||
|
||||
module "oauth_applications" {
|
||||
source = "./modules/oauth-provider"
|
||||
|
||||
for_each = var.oauth_applications
|
||||
|
||||
name = each.value.name
|
||||
app_name = each.value.name
|
||||
app_slug = each.value.slug
|
||||
app_group = each.value.group
|
||||
client_id = each.value.client_id
|
||||
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
|
||||
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||
redirect_uris = each.value.redirect_uris
|
||||
client_type = each.value.client_type
|
||||
include_claims_in_id_token = each.value.include_claims_in_id_token
|
||||
access_code_validity = each.value.access_code_validity
|
||||
access_token_validity = each.value.access_token_validity
|
||||
refresh_token_validity = each.value.refresh_token_validity
|
||||
property_mappings = each.value.property_mappings
|
||||
signing_key = each.value.signing_key
|
||||
policy_engine_mode = each.value.policy_engine_mode
|
||||
meta_description = each.value.meta_description
|
||||
meta_launch_url = each.value.meta_launch_url
|
||||
meta_icon = each.value.meta_icon
|
||||
scope_mappings = each.value.scope_mappings
|
||||
|
||||
# Access control - only pass explicitly defined groups
|
||||
access_groups = [
|
||||
for group_key in each.value.access_groups :
|
||||
try(
|
||||
authentik_group.root_groups[group_key].id,
|
||||
authentik_group.child_groups[group_key].id
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
module "proxy_applications" {
|
||||
source = "./modules/proxy-provider"
|
||||
|
||||
for_each = var.proxy_applications
|
||||
|
||||
name = each.value.name
|
||||
app_name = each.value.name
|
||||
app_slug = each.value.slug
|
||||
app_group = each.value.group
|
||||
external_host = each.value.external_host
|
||||
internal_host = each.value.internal_host
|
||||
internal_host_ssl_validation = each.value.internal_host_ssl_validation
|
||||
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
|
||||
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||
mode = each.value.mode
|
||||
intercept_header_auth = each.value.intercept_header_auth
|
||||
basic_auth_enabled = each.value.basic_auth_enabled
|
||||
basic_auth_user_attribute = each.value.basic_auth_username_attribute
|
||||
basic_auth_password_attribute = each.value.basic_auth_password_attribute
|
||||
cookie_domain = each.value.cookie_domain
|
||||
skip_path_regex = each.value.skip_path_regex
|
||||
policy_engine_mode = each.value.policy_engine_mode
|
||||
meta_description = each.value.meta_description
|
||||
meta_launch_url = each.value.meta_launch_url
|
||||
meta_icon = each.value.meta_icon
|
||||
|
||||
# Access control - only pass explicitly defined groups
|
||||
access_groups = [
|
||||
for group_key in each.value.access_groups :
|
||||
try(
|
||||
authentik_group.root_groups[group_key].id,
|
||||
authentik_group.child_groups[group_key].id
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
# Binding auto-created groups to their applications
|
||||
resource "authentik_policy_binding" "auto_group_bindings" {
|
||||
for_each = {
|
||||
for k, v in var.proxy_applications : k => v
|
||||
if v.create_group == true
|
||||
}
|
||||
|
||||
target = module.proxy_applications[each.key].application_uuid
|
||||
group = authentik_group.proxy_app_groups[each.key].id
|
||||
order = 100
|
||||
|
||||
depends_on = [
|
||||
module.proxy_applications,
|
||||
authentik_group.proxy_app_groups
|
||||
]
|
||||
}
|
||||
|
||||
# Binding auto-created groups to their OAuth applications
|
||||
resource "authentik_policy_binding" "oauth_auto_group_bindings" {
|
||||
for_each = {
|
||||
for k, v in var.oauth_applications : k => v
|
||||
if v.create_group == true
|
||||
}
|
||||
|
||||
target = module.oauth_applications[each.key].application_uuid
|
||||
group = authentik_group.oauth_app_groups[each.key].id
|
||||
order = 100
|
||||
|
||||
depends_on = [
|
||||
module.oauth_applications,
|
||||
authentik_group.oauth_app_groups
|
||||
]
|
||||
}
|
||||
|
||||
module "saml_applications" {
|
||||
source = "./modules/saml-provider"
|
||||
|
||||
for_each = var.saml_applications
|
||||
|
||||
name = each.value.name
|
||||
app_name = each.value.name
|
||||
app_slug = each.value.slug
|
||||
app_group = each.value.group
|
||||
authorization_flow = try(authentik_flow.flows[each.value.authorization_flow].id, data.authentik_flow.default_authorization_flow.id)
|
||||
invalidation_flow = data.authentik_flow.default_invalidation_flow.id
|
||||
acs_url = each.value.acs_url
|
||||
issuer = each.value.issuer
|
||||
audience = each.value.audience
|
||||
sp_binding = each.value.sp_binding
|
||||
signing_key = each.value.signing_key
|
||||
property_mappings = [for pm in each.value.property_mappings : authentik_property_mapping_provider_saml.saml_mappings[pm].id]
|
||||
name_id_mapping = each.value.name_id_mapping != null ? authentik_property_mapping_provider_saml.saml_mappings[each.value.name_id_mapping].id : null
|
||||
assertion_valid_not_before = each.value.assertion_valid_not_before
|
||||
assertion_valid_not_on_or_after = each.value.assertion_valid_not_on_or_after
|
||||
session_valid_not_on_or_after = each.value.session_valid_not_on_or_after
|
||||
policy_engine_mode = each.value.policy_engine_mode
|
||||
meta_description = each.value.meta_description
|
||||
meta_launch_url = each.value.meta_launch_url
|
||||
meta_icon = each.value.meta_icon
|
||||
}
|
||||
|
||||
locals {
|
||||
oauth_outpost_assignments = {
|
||||
for app_key, app in var.oauth_applications : app_key => app.outpost
|
||||
if app.outpost != null
|
||||
}
|
||||
|
||||
proxy_outpost_assignments = {
|
||||
for app_key, app in var.proxy_applications : app_key => app.outpost
|
||||
if app.outpost != null
|
||||
}
|
||||
|
||||
outpost_providers = {
|
||||
for outpost_key, outpost in var.outposts : outpost_key => concat(
|
||||
[for app_key, app_outpost in local.oauth_outpost_assignments :
|
||||
module.oauth_applications[app_key].provider_id if app_outpost == outpost_key],
|
||||
[for app_key, app_outpost in local.proxy_outpost_assignments :
|
||||
module.proxy_applications[app_key].provider_id if app_outpost == outpost_key]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
resource "authentik_outpost" "outposts" {
|
||||
for_each = {
|
||||
for k, v in var.outposts : k => v
|
||||
if length(lookup(local.outpost_providers, k, [])) > 0
|
||||
}
|
||||
|
||||
name = each.value.name
|
||||
type = "proxy"
|
||||
protocol_providers = local.outpost_providers[each.key]
|
||||
service_connection = data.authentik_service_connection_kubernetes.local_k8s.id
|
||||
config = jsonencode({
|
||||
log_level = "info"
|
||||
docker_labels = null
|
||||
authentik_host = var.authentik_url
|
||||
docker_network = null
|
||||
container_image = null
|
||||
docker_map_ports = true
|
||||
refresh_interval = "minutes=5"
|
||||
kubernetes_replicas = 1
|
||||
kubernetes_namespace = "authentik"
|
||||
authentik_host_browser = ""
|
||||
object_naming_template = "ak-outpost-%(name)s"
|
||||
authentik_host_insecure = false
|
||||
kubernetes_json_patches = {
|
||||
deployment = [
|
||||
{
|
||||
op = "add"
|
||||
path = "/spec/template/spec/containers/0/env/-"
|
||||
value = {
|
||||
name = "AUTHENTIK_POSTGRESQL__HOST"
|
||||
value = "psql.psql.svc"
|
||||
}
|
||||
},
|
||||
{
|
||||
op = "add"
|
||||
path = "/spec/template/spec/containers/0/env/-"
|
||||
value = {
|
||||
name = "AUTHENTIK_POSTGRESQL__PORT"
|
||||
value = "5432"
|
||||
}
|
||||
},
|
||||
{
|
||||
op = "add"
|
||||
path = "/spec/template/spec/containers/0/env/-"
|
||||
value = {
|
||||
name = "AUTHENTIK_POSTGRESQL__NAME"
|
||||
value = "authentik"
|
||||
}
|
||||
},
|
||||
{
|
||||
op = "add"
|
||||
path = "/spec/template/spec/containers/0/env/-"
|
||||
value = {
|
||||
name = "AUTHENTIK_POSTGRESQL__USER"
|
||||
valueFrom = {
|
||||
secretKeyRef = {
|
||||
name = "authentik-creds"
|
||||
key = "AUTHENTIK_POSTGRESQL__USER"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
op = "add"
|
||||
path = "/spec/template/spec/containers/0/env/-"
|
||||
value = {
|
||||
name = "AUTHENTIK_POSTGRESQL__PASSWORD"
|
||||
valueFrom = {
|
||||
secretKeyRef = {
|
||||
name = "authentik-creds"
|
||||
key = "AUTHENTIK_POSTGRESQL__PASSWORD"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
kubernetes_service_type = "ClusterIP"
|
||||
kubernetes_image_pull_secrets = []
|
||||
kubernetes_ingress_class_name = null
|
||||
kubernetes_disabled_components = []
|
||||
kubernetes_ingress_annotations = {}
|
||||
kubernetes_ingress_secret_name = "idm-tls"
|
||||
})
|
||||
|
||||
depends_on = [
|
||||
module.oauth_applications,
|
||||
module.proxy_applications
|
||||
]
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
authentik = {
|
||||
source = "goauthentik/authentik"
|
||||
version = ">= 2023.10.0"
|
||||
}
|
||||
random = {
|
||||
source = "hashicorp/random"
|
||||
version = ">= 3.5.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Get all available scope mappings
|
||||
data "authentik_property_mapping_provider_scope" "all_scopes" {
|
||||
managed_list = [
|
||||
"goauthentik.io/providers/oauth2/scope-email",
|
||||
"goauthentik.io/providers/oauth2/scope-openid",
|
||||
"goauthentik.io/providers/oauth2/scope-profile"
|
||||
]
|
||||
}
|
||||
|
||||
# Filter scope mappings based on requested scopes
|
||||
locals {
|
||||
scope_name_mapping = {
|
||||
"openid" = "goauthentik.io/providers/oauth2/scope-openid"
|
||||
"profile" = "goauthentik.io/providers/oauth2/scope-profile"
|
||||
"email" = "goauthentik.io/providers/oauth2/scope-email"
|
||||
}
|
||||
|
||||
selected_scope_ids = [
|
||||
for scope in var.scope_mappings :
|
||||
data.authentik_property_mapping_provider_scope.all_scopes.ids[index(data.authentik_property_mapping_provider_scope.all_scopes.managed_list, local.scope_name_mapping[scope])]
|
||||
if contains(keys(local.scope_name_mapping), scope)
|
||||
]
|
||||
}
|
||||
|
||||
resource "random_password" "client_secret" {
|
||||
count = var.client_secret == null ? 1 : 0
|
||||
length = 40
|
||||
special = true
|
||||
}
|
||||
|
||||
resource "authentik_provider_oauth2" "provider" {
|
||||
name = var.name
|
||||
client_id = var.client_id != null ? var.client_id : random_id.client_id[0].hex
|
||||
client_secret = var.client_secret != null ? var.client_secret : random_password.client_secret[0].result
|
||||
client_type = var.client_type
|
||||
authorization_flow = var.authorization_flow
|
||||
invalidation_flow = var.invalidation_flow
|
||||
include_claims_in_id_token = var.include_claims_in_id_token
|
||||
access_code_validity = var.access_code_validity
|
||||
access_token_validity = var.access_token_validity
|
||||
refresh_token_validity = var.refresh_token_validity
|
||||
signing_key = var.signing_key
|
||||
|
||||
allowed_redirect_uris = [
|
||||
for uri in var.redirect_uris : {
|
||||
matching_mode = "strict"
|
||||
url = uri
|
||||
}
|
||||
]
|
||||
|
||||
property_mappings = length(var.property_mappings) > 0 ? var.property_mappings : local.selected_scope_ids
|
||||
}
|
||||
|
||||
resource "random_id" "client_id" {
|
||||
count = var.client_id == null ? 1 : 0
|
||||
byte_length = 20
|
||||
}
|
||||
|
||||
resource "authentik_application" "app" {
|
||||
name = var.app_name
|
||||
slug = var.app_slug
|
||||
protocol_provider = authentik_provider_oauth2.provider.id
|
||||
group = var.app_group
|
||||
policy_engine_mode = var.policy_engine_mode
|
||||
meta_description = var.meta_description
|
||||
meta_launch_url = var.meta_launch_url
|
||||
meta_icon = var.meta_icon
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "app_access" {
|
||||
for_each = var.access_policies
|
||||
|
||||
target = authentik_application.app.id
|
||||
policy = each.value.policy_id
|
||||
order = each.value.order
|
||||
|
||||
enabled = lookup(each.value, "enabled", true)
|
||||
timeout = lookup(each.value, "timeout", 30)
|
||||
negate = lookup(each.value, "negate", false)
|
||||
failure_result = lookup(each.value, "failure_result", true)
|
||||
}
|
||||
|
||||
# Binding groups to the application
|
||||
resource "authentik_policy_binding" "group_bindings" {
|
||||
for_each = { for idx, group_id in var.access_groups : idx => group_id }
|
||||
|
||||
target = authentik_application.app.uuid
|
||||
group = each.value
|
||||
order = 10 + each.key
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
output "provider_id" {
|
||||
description = "ID of the OAuth2 provider"
|
||||
value = authentik_provider_oauth2.provider.id
|
||||
}
|
||||
|
||||
output "application_id" {
|
||||
description = "ID of the application"
|
||||
value = authentik_application.app.id
|
||||
}
|
||||
|
||||
output "application_uuid" {
|
||||
description = "UUID of the application"
|
||||
value = authentik_application.app.uuid
|
||||
}
|
||||
|
||||
output "client_id" {
|
||||
description = "OAuth2 Client ID"
|
||||
value = authentik_provider_oauth2.provider.client_id
|
||||
}
|
||||
|
||||
output "client_secret" {
|
||||
description = "OAuth2 Client Secret"
|
||||
value = authentik_provider_oauth2.provider.client_secret
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "application_slug" {
|
||||
description = "Application slug"
|
||||
value = authentik_application.app.slug
|
||||
}
|
||||
@@ -1,150 +0,0 @@
|
||||
variable "name" {
|
||||
description = "Name of the OAuth2 provider"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_name" {
|
||||
description = "Name of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_slug" {
|
||||
description = "Slug of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_group" {
|
||||
description = "Group for the application"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "client_id" {
|
||||
description = "OAuth2 Client ID"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "client_secret" {
|
||||
description = "OAuth2 Client Secret"
|
||||
type = string
|
||||
default = null
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "client_type" {
|
||||
description = "OAuth2 Client type (confidential or public)"
|
||||
type = string
|
||||
default = "confidential"
|
||||
|
||||
validation {
|
||||
condition = contains(["confidential", "public"], var.client_type)
|
||||
error_message = "Client type must be either 'confidential' or 'public'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "authorization_flow" {
|
||||
description = "Authorization flow UUID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "invalidation_flow" {
|
||||
description = "Invalidation flow UUID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "redirect_uris" {
|
||||
description = "List of allowed redirect URIs"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "access_code_validity" {
|
||||
description = "Access code validity duration"
|
||||
type = string
|
||||
default = "minutes=1"
|
||||
}
|
||||
|
||||
variable "access_token_validity" {
|
||||
description = "Access token validity duration"
|
||||
type = string
|
||||
default = "minutes=5"
|
||||
}
|
||||
|
||||
variable "refresh_token_validity" {
|
||||
description = "Refresh token validity duration"
|
||||
type = string
|
||||
default = "days=30"
|
||||
}
|
||||
|
||||
variable "include_claims_in_id_token" {
|
||||
description = "Include claims in ID token"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "signing_key" {
|
||||
description = "Signing key UUID"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "property_mappings" {
|
||||
description = "List of property mapping UUIDs"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "policy_engine_mode" {
|
||||
description = "Policy engine mode"
|
||||
type = string
|
||||
default = "all"
|
||||
|
||||
validation {
|
||||
condition = contains(["all", "any"], var.policy_engine_mode)
|
||||
error_message = "Policy engine mode must be either 'all' or 'any'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "meta_description" {
|
||||
description = "Application meta description"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_launch_url" {
|
||||
description = "Application launch URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_icon" {
|
||||
description = "Application icon URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "access_policies" {
|
||||
description = "Access policies for the application"
|
||||
type = map(object({
|
||||
policy_id = string
|
||||
order = number
|
||||
enabled = optional(bool, true)
|
||||
timeout = optional(number, 30)
|
||||
negate = optional(bool, false)
|
||||
failure_result = optional(bool, true)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "access_groups" {
|
||||
description = "List of group IDs that have access to the application"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "scope_mappings" {
|
||||
description = "List of scope mappings for the OAuth provider"
|
||||
type = list(string)
|
||||
default = ["openid", "profile", "email"]
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
authentik = {
|
||||
source = "goauthentik/authentik"
|
||||
version = ">= 2023.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "authentik_provider_proxy" "provider" {
|
||||
name = var.name
|
||||
external_host = var.external_host
|
||||
internal_host = var.internal_host
|
||||
internal_host_ssl_validation = var.internal_host_ssl_validation
|
||||
authorization_flow = var.authorization_flow
|
||||
invalidation_flow = var.invalidation_flow
|
||||
mode = var.mode
|
||||
cookie_domain = var.cookie_domain
|
||||
skip_path_regex = var.skip_path_regex
|
||||
intercept_header_auth = var.intercept_header_auth
|
||||
basic_auth_enabled = var.basic_auth_enabled
|
||||
basic_auth_password_attribute = var.basic_auth_password_attribute
|
||||
|
||||
property_mappings = var.property_mappings
|
||||
}
|
||||
|
||||
resource "authentik_application" "app" {
|
||||
name = var.app_name
|
||||
slug = var.app_slug
|
||||
protocol_provider = authentik_provider_proxy.provider.id
|
||||
group = var.app_group
|
||||
policy_engine_mode = var.policy_engine_mode
|
||||
meta_description = var.meta_description
|
||||
meta_launch_url = var.meta_launch_url
|
||||
meta_icon = var.meta_icon
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "app_access" {
|
||||
for_each = var.access_policies
|
||||
|
||||
target = authentik_application.app.id
|
||||
policy = each.value.policy_id
|
||||
order = each.value.order
|
||||
|
||||
enabled = lookup(each.value, "enabled", true)
|
||||
timeout = lookup(each.value, "timeout", 30)
|
||||
negate = lookup(each.value, "negate", false)
|
||||
failure_result = lookup(each.value, "failure_result", true)
|
||||
}
|
||||
|
||||
# Binding groups to the application
|
||||
resource "authentik_policy_binding" "group_bindings" {
|
||||
for_each = { for idx, group_id in var.access_groups : idx => group_id }
|
||||
|
||||
target = authentik_application.app.uuid
|
||||
group = each.value
|
||||
order = 10 + each.key
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
output "provider_id" {
|
||||
description = "ID of the Proxy provider"
|
||||
value = authentik_provider_proxy.provider.id
|
||||
}
|
||||
|
||||
output "application_id" {
|
||||
description = "ID of the application"
|
||||
value = authentik_application.app.id
|
||||
}
|
||||
|
||||
output "application_uuid" {
|
||||
description = "UUID of the application"
|
||||
value = authentik_application.app.uuid
|
||||
}
|
||||
|
||||
output "application_slug" {
|
||||
description = "Application slug"
|
||||
value = authentik_application.app.slug
|
||||
}
|
||||
|
||||
output "launch_url" {
|
||||
description = "Application launch URL"
|
||||
value = authentik_application.app.meta_launch_url
|
||||
}
|
||||
|
||||
output "external_host" {
|
||||
description = "External host URL"
|
||||
value = authentik_provider_proxy.provider.external_host
|
||||
}
|
||||
|
||||
output "internal_host" {
|
||||
description = "Internal host URL"
|
||||
value = authentik_provider_proxy.provider.internal_host
|
||||
}
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
variable "name" {
|
||||
description = "Name of the Proxy provider"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_name" {
|
||||
description = "Name of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_slug" {
|
||||
description = "Slug of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_group" {
|
||||
description = "Group for the application"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "external_host" {
|
||||
description = "External hostname for the proxy"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "internal_host" {
|
||||
description = "Internal hostname for the proxy"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "internal_host_ssl_validation" {
|
||||
description = "Enable SSL validation for internal host"
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "authorization_flow" {
|
||||
description = "Authorization flow UUID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "invalidation_flow" {
|
||||
description = "Invalidation flow UUID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "mode" {
|
||||
description = "Proxy mode (proxy, forward_single, forward_domain)"
|
||||
type = string
|
||||
default = "proxy"
|
||||
|
||||
validation {
|
||||
condition = contains(["proxy", "forward_single", "forward_domain"], var.mode)
|
||||
error_message = "Mode must be one of: proxy, forward_single, forward_domain."
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
variable "cookie_domain" {
|
||||
description = "Cookie domain for the proxy"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
|
||||
variable "skip_path_regex" {
|
||||
description = "Regular expression for paths to skip authentication"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "intercept_header_auth" {
|
||||
description = "Intercept header authentication"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "basic_auth_enabled" {
|
||||
description = "Enable basic authentication"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
|
||||
variable "basic_auth_password_attribute" {
|
||||
description = "Attribute for basic auth password"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "basic_auth_user_attribute" {
|
||||
description = "Attribute for basic auth username"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "property_mappings" {
|
||||
description = "List of property mapping UUIDs"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "policy_engine_mode" {
|
||||
description = "Policy engine mode"
|
||||
type = string
|
||||
default = "all"
|
||||
|
||||
validation {
|
||||
condition = contains(["all", "any"], var.policy_engine_mode)
|
||||
error_message = "Policy engine mode must be either 'all' or 'any'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "meta_description" {
|
||||
description = "Application meta description"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_launch_url" {
|
||||
description = "Application launch URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_icon" {
|
||||
description = "Application icon URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
|
||||
variable "access_policies" {
|
||||
description = "Access policies for the application"
|
||||
type = map(object({
|
||||
policy_id = string
|
||||
order = number
|
||||
enabled = optional(bool, true)
|
||||
timeout = optional(number, 30)
|
||||
negate = optional(bool, false)
|
||||
failure_result = optional(bool, true)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "access_groups" {
|
||||
description = "List of group IDs that have access to the application"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
authentik = {
|
||||
source = "goauthentik/authentik"
|
||||
version = ">= 2023.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data "authentik_certificate_key_pair" "default" {
|
||||
name = "authentik Self-signed Certificate"
|
||||
}
|
||||
|
||||
resource "authentik_provider_saml" "provider" {
|
||||
name = var.name
|
||||
authorization_flow = var.authorization_flow
|
||||
invalidation_flow = var.invalidation_flow
|
||||
acs_url = var.acs_url
|
||||
issuer = var.issuer
|
||||
audience = var.audience
|
||||
sp_binding = var.sp_binding
|
||||
signing_kp = var.signing_key != null ? var.signing_key : data.authentik_certificate_key_pair.default.id
|
||||
property_mappings = var.property_mappings
|
||||
name_id_mapping = var.name_id_mapping
|
||||
|
||||
assertion_valid_not_before = var.assertion_valid_not_before
|
||||
assertion_valid_not_on_or_after = var.assertion_valid_not_on_or_after
|
||||
session_valid_not_on_or_after = var.session_valid_not_on_or_after
|
||||
}
|
||||
|
||||
resource "authentik_application" "app" {
|
||||
name = var.app_name
|
||||
slug = var.app_slug
|
||||
protocol_provider = authentik_provider_saml.provider.id
|
||||
group = var.app_group
|
||||
policy_engine_mode = var.policy_engine_mode
|
||||
meta_description = var.meta_description
|
||||
meta_launch_url = var.meta_launch_url
|
||||
meta_icon = var.meta_icon
|
||||
}
|
||||
|
||||
resource "authentik_policy_binding" "app_access" {
|
||||
for_each = var.access_policies
|
||||
|
||||
target = authentik_application.app.id
|
||||
policy = each.value.policy_id
|
||||
order = each.value.order
|
||||
|
||||
enabled = lookup(each.value, "enabled", true)
|
||||
timeout = lookup(each.value, "timeout", 30)
|
||||
negate = lookup(each.value, "negate", false)
|
||||
failure_result = lookup(each.value, "failure_result", true)
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
output "provider_id" {
|
||||
description = "ID of the SAML provider"
|
||||
value = authentik_provider_saml.provider.id
|
||||
}
|
||||
|
||||
output "application_id" {
|
||||
description = "ID of the application"
|
||||
value = authentik_application.app.id
|
||||
}
|
||||
|
||||
output "provider_name" {
|
||||
description = "Name of the SAML provider"
|
||||
value = authentik_provider_saml.provider.name
|
||||
}
|
||||
|
||||
output "acs_url" {
|
||||
description = "Assertion Consumer Service URL"
|
||||
value = authentik_provider_saml.provider.acs_url
|
||||
}
|
||||
|
||||
output "issuer" {
|
||||
description = "SAML Issuer"
|
||||
value = authentik_provider_saml.provider.issuer
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
variable "name" {
|
||||
description = "Name of the SAML provider"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_name" {
|
||||
description = "Name of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_slug" {
|
||||
description = "Slug of the application"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "app_group" {
|
||||
description = "Group of the application"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "authorization_flow" {
|
||||
description = "Authorization flow ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "invalidation_flow" {
|
||||
description = "Invalidation flow ID"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "acs_url" {
|
||||
description = "Assertion Consumer Service URL"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "issuer" {
|
||||
description = "SAML Issuer"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "audience" {
|
||||
description = "SAML Audience"
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "sp_binding" {
|
||||
description = "Service Provider binding (post or redirect)"
|
||||
type = string
|
||||
default = "post"
|
||||
}
|
||||
|
||||
variable "signing_key" {
|
||||
description = "Certificate key pair ID for signing"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "property_mappings" {
|
||||
description = "List of property mapping IDs"
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "name_id_mapping" {
|
||||
description = "Property mapping ID for NameID"
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "assertion_valid_not_before" {
|
||||
description = "Assertion valid not before"
|
||||
type = string
|
||||
default = "minutes=-5"
|
||||
}
|
||||
|
||||
variable "assertion_valid_not_on_or_after" {
|
||||
description = "Assertion valid not on or after"
|
||||
type = string
|
||||
default = "minutes=5"
|
||||
}
|
||||
|
||||
variable "session_valid_not_on_or_after" {
|
||||
description = "Session valid not on or after"
|
||||
type = string
|
||||
default = "minutes=86400"
|
||||
}
|
||||
|
||||
variable "policy_engine_mode" {
|
||||
description = "Policy engine mode"
|
||||
type = string
|
||||
default = "all"
|
||||
}
|
||||
|
||||
variable "meta_description" {
|
||||
description = "Application description"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_launch_url" {
|
||||
description = "Application launch URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "meta_icon" {
|
||||
description = "Application icon URL"
|
||||
type = string
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "access_policies" {
|
||||
description = "Access policies for the application"
|
||||
type = map(object({
|
||||
policy_id = string
|
||||
order = number
|
||||
enabled = optional(bool, true)
|
||||
timeout = optional(number, 30)
|
||||
negate = optional(bool, false)
|
||||
failure_result = optional(bool, true)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
@@ -1,232 +0,0 @@
|
||||
oauth_applications = {
|
||||
"paperless" = {
|
||||
name = "Paperless-NGX"
|
||||
slug = "paperless"
|
||||
group = "Tools"
|
||||
meta_description = "Document management system"
|
||||
meta_icon = "https://img.icons8.com/fluency/48/documents.png"
|
||||
redirect_uris = ["https://docs.hexor.cy/accounts/oidc/authentik/login/callback/"]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"gitea" = {
|
||||
name = "Gitea"
|
||||
slug = "gitea"
|
||||
group = "Tools"
|
||||
meta_description = "Git repository hosting"
|
||||
meta_icon = "https://img.icons8.com/?size=100&id=20906&format=png&color=000000"
|
||||
redirect_uris = ["https://gt.hexor.cy/user/oauth2/Authentik/callback"]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=10"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"jellyfin" = {
|
||||
name = "Jellyfin"
|
||||
slug = "jellyfin"
|
||||
group = "Media and Storage"
|
||||
meta_description = "Media streaming server"
|
||||
meta_icon = "https://img.icons8.com/plasticine/100/jellyfin.png"
|
||||
redirect_uris = [
|
||||
"https://jf.hexor.cy/sso/OID/r/authentik",
|
||||
"https://jf.hexor.cy/sso/OID/redirect/authentik"
|
||||
]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=10"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"argocd" = {
|
||||
name = "ArgoCD"
|
||||
slug = "argocd"
|
||||
group = "Core"
|
||||
meta_description = "GitOps deployment tool"
|
||||
meta_icon = "https://img.icons8.com/color-glass/48/octopus.png"
|
||||
redirect_uris = ["https://ag.hexor.cy/auth/callback"]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"grafana" = {
|
||||
name = "Grafana"
|
||||
slug = "grafana"
|
||||
group = "Core"
|
||||
meta_description = "Monitoring and observability"
|
||||
meta_icon = "https://img.icons8.com/fluency/48/grafana.png"
|
||||
redirect_uris = ["https://gf.hexor.cy/login/generic_oauth"]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"immich" = {
|
||||
name = "Immich"
|
||||
slug = "immich"
|
||||
group = "Media and Storage"
|
||||
meta_description = "Photo and video management"
|
||||
meta_icon = "https://img.icons8.com/fluency/48/photos.png"
|
||||
redirect_uris = [
|
||||
"https://photos.hexor.cy/auth/login",
|
||||
"https://photos.hexor.cy/user-settings",
|
||||
"app.immich:///oauth-callback",
|
||||
"http://photos.homenet:30283/auth/login",
|
||||
"http://photos.homenet:30283/user-settings"
|
||||
]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
access_groups = ["admins"]
|
||||
create_group = true
|
||||
}
|
||||
|
||||
"pgadmin" = {
|
||||
name = "Postgres WEB Admin"
|
||||
slug = "pgadmin"
|
||||
group = "Core"
|
||||
meta_description = "PostgreSQL WEB administration"
|
||||
meta_icon = "https://img.icons8.com/?size=100&id=JRnxU7ZWP4mi&format=png&color=000000"
|
||||
redirect_uris = ["https://pg.hexor.cy/oauth2/authorize"]
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
|
||||
"home-assistant-lms" = {
|
||||
name = "Home Assistant LMS"
|
||||
slug = "home-assistant-lms"
|
||||
group = "Internal"
|
||||
meta_description = "Home Assistant Limassol"
|
||||
meta_icon = "https://img.icons8.com/stickers/100/smart-home-automation.png"
|
||||
redirect_uris = [
|
||||
"http://ha-lms:8123/auth/oidc/callback",
|
||||
"http://ha-lms.homenet:8123/auth/oidc/callback",
|
||||
]
|
||||
meta_launch_url = "http://ha-lms:8123/auth/oidc/welcome"
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
create_group = true
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
"home-assistant-london" = {
|
||||
name = "Home Assistant London"
|
||||
slug = "home-assistant-london"
|
||||
group = "Internal"
|
||||
meta_description = "Home Assistant London"
|
||||
meta_icon = "https://img.icons8.com/stickers/100/smart-home-automation.png"
|
||||
redirect_uris = [
|
||||
"http://ha-london:8123/auth/oidc/callback",
|
||||
"http://ha-london.tail2fe2d.ts.net:8123/auth/oidc/callback",
|
||||
]
|
||||
meta_launch_url = "http://ha-london:8123/auth/oidc/welcome"
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
create_group = true
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
|
||||
"openwebui" = {
|
||||
name = "OpenWeb UI"
|
||||
slug = "openwebui"
|
||||
group = "Tools"
|
||||
meta_description = "OpenWeb UI"
|
||||
meta_icon = "https://ollama.com/public/ollama.png"
|
||||
redirect_uris = [
|
||||
"https://ai.hexor.cy/oauth/oidc/callback",
|
||||
]
|
||||
meta_launch_url = "https://ai.hexor.cy"
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = ["admins"]
|
||||
create_group = true
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
"matrix" = {
|
||||
name = "Matrix Chat"
|
||||
slug = "matrix"
|
||||
group = "Tools"
|
||||
meta_description = "Matrix Chat"
|
||||
meta_icon = "https://img.icons8.com/ios/100/40C057/matrix-logo.png"
|
||||
redirect_uris = [
|
||||
"https://auth.matrix.hexor.cy/upstream/callback/001KKV4EKY7KG98W2M9T806K6A",
|
||||
]
|
||||
meta_launch_url = "https://chat.matrix.hexor.cy"
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = []
|
||||
create_group = false
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
"furumi-ng-web" = {
|
||||
name = "Furumi Web Player"
|
||||
slug = "furumi-ng-web"
|
||||
group = "Tools"
|
||||
meta_description = "Furumi Web Player"
|
||||
meta_icon = "https://img.icons8.com/pulsar-color/48/music.png"
|
||||
redirect_uris = [
|
||||
"https://music.hexor.cy/auth/callback",
|
||||
]
|
||||
meta_launch_url = "https://music.hexor.cy"
|
||||
client_type = "confidential"
|
||||
include_claims_in_id_token = true
|
||||
access_code_validity = "minutes=1"
|
||||
access_token_validity = "minutes=5"
|
||||
refresh_token_validity = "days=30"
|
||||
scope_mappings = ["openid", "profile", "email"]
|
||||
access_groups = []
|
||||
create_group = true
|
||||
signing_key = "1b1b5bec-034a-4d96-871a-133f11322360"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
output "oauth_applications" {
|
||||
description = "OAuth2/OpenID applications details"
|
||||
value = {
|
||||
for k, v in module.oauth_applications : k => {
|
||||
application_id = v.application_id
|
||||
application_uuid = v.application_uuid
|
||||
client_id = v.client_id
|
||||
client_secret = v.client_secret
|
||||
slug = v.application_slug
|
||||
}
|
||||
}
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
output "proxy_applications" {
|
||||
description = "Proxy applications details"
|
||||
value = {
|
||||
for k, v in module.proxy_applications : k => {
|
||||
application_id = v.application_id
|
||||
application_uuid = v.application_uuid
|
||||
external_host = v.external_host
|
||||
internal_host = v.internal_host
|
||||
slug = v.application_slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "outposts" {
|
||||
description = "Outposts details"
|
||||
value = {
|
||||
for k, v in authentik_outpost.outposts : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
type = v.type
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "groups" {
|
||||
description = "Groups details"
|
||||
value = merge(
|
||||
{
|
||||
for k, v in authentik_group.root_groups : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
}
|
||||
},
|
||||
{
|
||||
for k, v in authentik_group.child_groups : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
}
|
||||
},
|
||||
{
|
||||
for k, v in authentik_group.proxy_app_groups : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
auto_created = true
|
||||
type = "proxy"
|
||||
}
|
||||
},
|
||||
{
|
||||
for k, v in authentik_group.oauth_app_groups : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
auto_created = true
|
||||
type = "oauth"
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
output "flows" {
|
||||
description = "Custom flows details"
|
||||
value = {
|
||||
for k, v in authentik_flow.flows : k => {
|
||||
id = v.id
|
||||
slug = v.slug
|
||||
name = v.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output "certificates" {
|
||||
description = "Certificates details"
|
||||
value = {
|
||||
for k, v in authentik_certificate_key_pair.certificates : k => {
|
||||
id = v.id
|
||||
name = v.name
|
||||
fingerprint_sha256 = v.fingerprint_sha256
|
||||
fingerprint_sha1 = v.fingerprint_sha1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Output for applications table generation
|
||||
output "applications_for_wiki" {
|
||||
description = "Applications data formatted for wiki table generation"
|
||||
value = {
|
||||
proxy_apps = {
|
||||
for k, v in var.proxy_applications : k => {
|
||||
name = v.name
|
||||
type = "Proxy"
|
||||
url = v.external_host
|
||||
internal_url = v.internal_host
|
||||
group = v.group
|
||||
description = v.meta_description
|
||||
icon = v.meta_icon
|
||||
slug = v.slug
|
||||
}
|
||||
}
|
||||
oauth_apps = {
|
||||
for k, v in var.oauth_applications : k => {
|
||||
name = v.name
|
||||
type = "OAuth2/OpenID"
|
||||
url = length(v.redirect_uris) > 0 ? "https://${split("/", replace(v.redirect_uris[0], "https://", ""))[0]}" : ""
|
||||
group = v.group
|
||||
description = v.meta_description
|
||||
icon = v.meta_icon
|
||||
slug = v.slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
authentik = {
|
||||
source = "goauthentik/authentik"
|
||||
version = "2025.12.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "authentik" {
|
||||
url = var.authentik_url
|
||||
token = var.authentik_token
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
proxy_applications = {
|
||||
"k8s-dashboard" = {
|
||||
name = "K8S dashboard"
|
||||
slug = "k8s-dashboard-ns"
|
||||
group = "Core"
|
||||
external_host = "https://k8s.hexor.cy"
|
||||
internal_host = "http://kubernetes-dashboard.kubernetes-dashboard.svc"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = "K8S dashboard chart"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
meta_icon = "https://img.icons8.com/color/48/kubernetes.png"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
"filemanager" = {
|
||||
name = "FM filemanager"
|
||||
slug = "fm-filemanager"
|
||||
group = "Core"
|
||||
external_host = "https://fm.hexor.cy"
|
||||
internal_host = "http://fb-filemanager-filebrowser.syncthing.svc"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = "K8S dashboard chart"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
meta_icon = "https://img.icons8.com/external-anggara-flat-anggara-putra/32/external-folder-basic-user-interface-anggara-flat-anggara-putra.png"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"prometheus" = {
|
||||
name = "Prometheus"
|
||||
slug = "prometheus"
|
||||
group = "Core"
|
||||
external_host = "https://prom.hexor.cy"
|
||||
internal_host = "http://prometheus-kube-prometheus-prometheus.prometheus.svc:9090"
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/fluency/48/prometheus-app.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
internal_host_ssl_validation = false
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"mtproxy-links" = {
|
||||
name = "mtproxy-links"
|
||||
slug = "mtproxy-links"
|
||||
group = "Core"
|
||||
external_host = "https://proxy.hexor.cy"
|
||||
internal_host = "http://secret-reader.mtproxy.svc:80"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
skip_path_regex = <<-EOT
|
||||
/webhook
|
||||
EOT
|
||||
meta_icon = "https://img.icons8.com/ios-filled/50/password.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
# Tools applications
|
||||
"qbittorrent" = {
|
||||
name = "qBittorent"
|
||||
slug = "qbittorent"
|
||||
group = "Tools"
|
||||
external_host = "https://qbt.hexor.cy"
|
||||
internal_host = "http://qbittorrent.jellyfin.svc"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/nolan/64/qbittorrent--v2.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
# Media and Storage applications
|
||||
"kopia" = {
|
||||
name = "Kopia"
|
||||
slug = "kopia"
|
||||
group = "Media and Storage"
|
||||
external_host = "https://backup.hexor.cy"
|
||||
internal_host = "http://100.72.135.2:51515"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/external-flaticons-lineal-color-flat-icons/64/external-backup-productivity-flaticons-lineal-color-flat-icons.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"syncthing-router" = {
|
||||
name = "Syncthing"
|
||||
slug = "syncthing"
|
||||
group = "Media and Storage"
|
||||
external_host = "https://ss.hexor.cy"
|
||||
internal_host = "http://syncthing-router.syncthing.svc:80"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/?size=100&id=Id4NcEcXcYzF&format=png&color=000000"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"truenas" = {
|
||||
name = "TrueNAS"
|
||||
slug = "truenas-proxy"
|
||||
group = "Media and Storage"
|
||||
external_host = "https://nas.hexor.cy"
|
||||
internal_host = "http://10.0.5.107:81"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/dusk/64/nas.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"khm" = {
|
||||
name = "KHM"
|
||||
slug = "khm"
|
||||
group = "Media and Storage"
|
||||
external_host = "https://khm.hexor.cy"
|
||||
internal_host = "http://khm.khm.svc:8080"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/liquid-glass/48/key.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
access_groups = ["admins", "khm"]
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
|
||||
"minecraft" = {
|
||||
name = "Minecraft"
|
||||
slug = "minecraft"
|
||||
group = "Media and Storage"
|
||||
external_host = "https://minecraft.hexor.cy"
|
||||
internal_host = "http://minecraft-dynmap.minecraft.svc"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/color/48/minecraft-grass-cube.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
skip_path_regex = <<-EOT
|
||||
/clients
|
||||
EOT
|
||||
}
|
||||
"pasarguard" = {
|
||||
name = "PasarGuard"
|
||||
slug = "pasarguard"
|
||||
group = "Tools"
|
||||
external_host = "https://ps.hexor.cy"
|
||||
internal_host = "https://pasarguard.pasarguard.svc:80"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
skip_path_regex = <<-EOT
|
||||
/
|
||||
/sub/
|
||||
/dashboard/
|
||||
/api/
|
||||
EOT
|
||||
meta_icon = "https://img.icons8.com/?size=100&id=fqAD3lAB6zTe&format=png&color=000000"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
"ollama-public" = {
|
||||
name = "Ollama Public"
|
||||
slug = "ollama-public"
|
||||
group = "AI"
|
||||
external_host = "https://ollama.hexor.cy"
|
||||
internal_host = "http://ollama.ollama.svc:11434"
|
||||
internal_host_ssl_validation = false
|
||||
meta_description = ""
|
||||
meta_icon = "https://img.icons8.com/external-icongeek26-outline-icongeek26/64/external-llama-animal-head-icongeek26-outline-icongeek26.png"
|
||||
mode = "proxy"
|
||||
outpost = "kubernetes-outpost"
|
||||
intercept_header_auth = true
|
||||
create_group = true
|
||||
access_groups = ["admins"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
terraform {
|
||||
cloud {
|
||||
organization = "ultradesu"
|
||||
workspaces {
|
||||
name = "Authentik"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
authentik_url = "https://idm.hexor.cy"
|
||||
|
||||
|
||||
flows = {
|
||||
}
|
||||
|
||||
tags = {
|
||||
environment = "production"
|
||||
managed_by = "terraform"
|
||||
project = "homelab"
|
||||
}
|
||||
|
||||
|
||||
|
||||
outposts = {
|
||||
"kubernetes-outpost" = {
|
||||
name = "authentik Embedded Outpost"
|
||||
type = "proxy"
|
||||
service_connection = "k8s-cluster"
|
||||
config = {
|
||||
authentik_host = "https://idm.hexor.cy"
|
||||
authentik_host_insecure = false
|
||||
log_level = "info"
|
||||
error_reporting = true
|
||||
#container_image = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
variable "oauth_applications" {
|
||||
description = "Map of OAuth2/OpenID applications"
|
||||
type = map(object({
|
||||
name = string
|
||||
slug = string
|
||||
group = optional(string, "")
|
||||
policy_engine_mode = optional(string, "any")
|
||||
meta_description = optional(string, "")
|
||||
meta_launch_url = optional(string, "")
|
||||
meta_icon = optional(string, "")
|
||||
redirect_uris = list(string)
|
||||
client_type = optional(string, "confidential")
|
||||
client_id = optional(string, null)
|
||||
include_claims_in_id_token = optional(bool, true)
|
||||
access_code_validity = optional(string, "minutes=1")
|
||||
access_token_validity = optional(string, "minutes=5")
|
||||
refresh_token_validity = optional(string, "days=30")
|
||||
property_mappings = optional(list(string), [])
|
||||
authorization_flow = optional(string, null)
|
||||
signing_key = optional(string, null)
|
||||
outpost = optional(string, null)
|
||||
create_group = optional(bool, false)
|
||||
access_groups = optional(list(string), [])
|
||||
scope_mappings = optional(list(string), ["openid", "profile", "email"])
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "proxy_applications" {
|
||||
description = "Map of Proxy applications"
|
||||
type = map(object({
|
||||
name = string
|
||||
slug = string
|
||||
group = optional(string, "")
|
||||
policy_engine_mode = optional(string, "any")
|
||||
meta_description = optional(string, "")
|
||||
meta_launch_url = optional(string, "")
|
||||
meta_icon = optional(string, "")
|
||||
external_host = string
|
||||
internal_host = optional(string, "")
|
||||
internal_host_ssl_validation = optional(bool, true)
|
||||
mode = optional(string, "proxy")
|
||||
intercept_header_auth = optional(bool, false)
|
||||
basic_auth_enabled = optional(bool, false)
|
||||
basic_auth_username_attribute = optional(string, "")
|
||||
basic_auth_password_attribute = optional(string, "")
|
||||
cookie_domain = optional(string, "")
|
||||
authorization_flow = optional(string, null)
|
||||
skip_path_regex = optional(string, "")
|
||||
outpost = optional(string, null)
|
||||
create_group = optional(bool, false)
|
||||
access_groups = optional(list(string), [])
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "saml_applications" {
|
||||
description = "Map of SAML applications"
|
||||
type = map(object({
|
||||
name = string
|
||||
slug = string
|
||||
group = optional(string, "")
|
||||
policy_engine_mode = optional(string, "any")
|
||||
meta_description = optional(string, "")
|
||||
meta_launch_url = optional(string, "")
|
||||
meta_icon = optional(string, "")
|
||||
acs_url = string
|
||||
issuer = string
|
||||
audience = string
|
||||
sp_binding = optional(string, "post")
|
||||
signing_key = optional(string, null)
|
||||
property_mappings = optional(list(string), [])
|
||||
name_id_mapping = optional(string, null)
|
||||
assertion_valid_not_before = optional(string, "minutes=-5")
|
||||
assertion_valid_not_on_or_after = optional(string, "minutes=5")
|
||||
session_valid_not_on_or_after = optional(string, "minutes=86400")
|
||||
authorization_flow = optional(string, null)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "outposts" {
|
||||
description = "Map of Outposts (only proxy type supported)"
|
||||
type = map(object({
|
||||
name = string
|
||||
config = optional(map(any), {})
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "flows" {
|
||||
description = "Map of authentication flows"
|
||||
type = map(object({
|
||||
name = string
|
||||
title = string
|
||||
slug = string
|
||||
designation = string
|
||||
policy_engine_mode = optional(string, "any")
|
||||
compatibility_mode = optional(bool, false)
|
||||
layout = optional(string, "stacked")
|
||||
denied_action = optional(string, "message_continue")
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "groups" {
|
||||
description = "Map of user groups"
|
||||
type = map(object({
|
||||
name = string
|
||||
is_superuser = optional(bool, false)
|
||||
parent = optional(string, null)
|
||||
attributes = optional(map(any), {})
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "certificates" {
|
||||
description = "Map of certificates for HTTPS"
|
||||
type = map(object({
|
||||
name = string
|
||||
certificate_data = string
|
||||
key_data = string
|
||||
managed = optional(string, null)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "property_mappings" {
|
||||
description = "Custom property mappings for SAML/OAuth"
|
||||
type = map(object({
|
||||
name = string
|
||||
expression = string
|
||||
saml_name = optional(string, null)
|
||||
oidc_scope = optional(string, null)
|
||||
}))
|
||||
default = {}
|
||||
}
|
||||
|
||||
|
||||
variable "default_authorization_flow" {
|
||||
description = "Default authorization flow slug"
|
||||
type = string
|
||||
default = "default-provider-authorization-implicit-consent"
|
||||
}
|
||||
|
||||
variable "default_authentication_flow" {
|
||||
description = "Default authentication flow slug"
|
||||
type = string
|
||||
default = "default-authentication-flow"
|
||||
}
|
||||
|
||||
variable "default_invalidation_flow" {
|
||||
description = "Default invalidation flow slug"
|
||||
type = string
|
||||
default = "default-provider-invalidation-flow"
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
description = "Tags to apply to all resources"
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "authentik_url" {
|
||||
description = "Authentik URL"
|
||||
type = string
|
||||
default = "https://idm.hexor.cy"
|
||||
}
|
||||
|
||||
variable "authentik_token" {
|
||||
description = "Authentik API token (set via TF_VAR_authentik_token env var)"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
@@ -20,6 +20,10 @@ proxy_applications = {
|
||||
domain = "prom.hexor.cy"
|
||||
allowed_groups = ["hexor-admin"]
|
||||
}
|
||||
FireflyImporter = {
|
||||
domain = "import.hexor.cy"
|
||||
allowed_groups = ["hexor-admin"]
|
||||
}
|
||||
}
|
||||
|
||||
oauth2_applications = {
|
||||
@@ -70,7 +74,7 @@ oauth2_applications = {
|
||||
post_logout_redirect_uris = ["https://docs.hexor.cy/*"]
|
||||
}
|
||||
Immich = {
|
||||
redirect_uris = [
|
||||
redirect_uris = [
|
||||
"https://photos.hexor.cy/auth/login",
|
||||
"https://photos.hexor.cy/user-settings",
|
||||
"app.immich:///oauth-callback",
|
||||
|
||||
Reference in New Issue
Block a user