diff --git a/k8s/apps/amnezia/app.yaml b/k8s/apps/amnezia/app.yaml new file mode 100644 index 0000000..9f2c007 --- /dev/null +++ b/k8s/apps/amnezia/app.yaml @@ -0,0 +1,20 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: amnezia + namespace: argocd +spec: + project: apps + destination: + namespace: amnezia + server: https://kubernetes.default.svc + source: + repoURL: ssh://git@gt.hexor.cy:30022/ab/homelab.git + targetRevision: HEAD + path: k8s/apps/amnezia + syncPolicy: + automated: + selfHeal: true + prune: true + syncOptions: + - CreateNamespace=true diff --git a/k8s/apps/amnezia/configmap-scripts.yaml b/k8s/apps/amnezia/configmap-scripts.yaml new file mode 100644 index 0000000..3471703 --- /dev/null +++ b/k8s/apps/amnezia/configmap-scripts.yaml @@ -0,0 +1,151 @@ +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: amneziawg-scripts +data: + firewall-up.sh: | + #!/usr/bin/env bash + set -euo pipefail + + PORT="${1:-5847}" + VPN_CIDR="${2:-10.8.1.0/24}" + + 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() { + local table_args=() + if [ "${1:-}" = "-t" ]; then + table_args=("$1" "$2") + shift 2 + fi + + local chain="$1" + shift + + if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then + iptables "${table_args[@]}" -I "${chain}" 1 "$@" + fi + } + + ensure_append_rule() { + local table_args=() + if [ "${1:-}" = "-t" ]; then + table_args=("$1" "$2") + shift 2 + fi + + local chain="$1" + shift + + if ! iptables "${table_args[@]}" -C "${chain}" "$@" >/dev/null 2>&1; then + iptables "${table_args[@]}" -A "${chain}" "$@" + fi + } + + 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" + exit 1 + fi + + sysctl -w net.ipv4.ip_forward=1 + + 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 + + firewall-down.sh: | + #!/usr/bin/env bash + set -euo pipefail + + PORT="${1:-5847}" + VPN_CIDR="${2:-10.8.1.0/24}" + + external_interface() { + ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i=="dev") {print $(i+1); exit}}' + } + + delete_rule() { + local table_args=() + if [ "${1:-}" = "-t" ]; then + table_args=("$1" "$2") + shift 2 + fi + + local chain="$1" + shift + + while iptables "${table_args[@]}" -D "${chain}" "$@" >/dev/null 2>&1; do + true + done + } + + EXT_IF="$(external_interface || true)" + if [ -z "${EXT_IF}" ]; then + EXT_IF="$(ip route show default | awk '{print $5; exit}')" + fi + + if [ -n "${EXT_IF}" ]; then + 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 + + 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 + + run.sh: | + #!/usr/bin/env bash + set -euo pipefail + + SERVER_CONFIG="/etc/amnezia/server/awg0.conf" + CLIENTS_DIR="/etc/amnezia/clients" + RUNTIME_CONFIG="/run/amnezia/awg0.conf" + + cleanup() { + if awg show awg0 >/dev/null 2>&1; then + awg-quick down "${RUNTIME_CONFIG}" || ip link delete awg0 || true + fi + } + + render_config() { + mkdir -p "$(dirname "${RUNTIME_CONFIG}")" + cp "${SERVER_CONFIG}" "${RUNTIME_CONFIG}" + chmod 0600 "${RUNTIME_CONFIG}" + + local clients_found=0 + for client_config in "${CLIENTS_DIR}"/*; do + [ -f "${client_config}" ] || continue + [ -s "${client_config}" ] || continue + printf '\n' >> "${RUNTIME_CONFIG}" + cat "${client_config}" >> "${RUNTIME_CONFIG}" + clients_found=1 + done + + if [ "${clients_found}" = "0" ]; then + echo "No client peer configs found in ${CLIENTS_DIR}; starting without peers" + fi + } + + trap cleanup EXIT + trap 'exit 0' TERM INT + + render_config + cleanup + awg-quick up "${RUNTIME_CONFIG}" + awg show awg0 || true + + while true; do + sleep 3600 & + wait "$!" + done diff --git a/k8s/apps/amnezia/daemonset.yaml b/k8s/apps/amnezia/daemonset.yaml new file mode 100644 index 0000000..640a561 --- /dev/null +++ b/k8s/apps/amnezia/daemonset.yaml @@ -0,0 +1,148 @@ +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: amneziawg + labels: + app: amneziawg + annotations: + reloader.stakater.com/auto: "true" + secret.reloader.stakater.com/reload: "amneziawg-server,amneziawg-clients" +spec: + selector: + matchLabels: + app: amneziawg + updateStrategy: + type: RollingUpdate + template: + metadata: + labels: + app: amneziawg + spec: + serviceAccountName: amneziawg + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + nodeSelector: + amnezia-vpn: "true" + tolerations: + - operator: Exists + initContainers: + - name: register-endpoint + image: bitnami/kubectl:latest + imagePullPolicy: IfNotPresent + env: + - name: NODE_NAME + valueFrom: + fieldRef: + fieldPath: spec.nodeName + - name: PORT + value: "5847" + command: + - /bin/bash + - -lc + - | + set -euo pipefail + + NAMESPACE="$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)" + ENDPOINT="$(kubectl get node "${NODE_NAME}" -o jsonpath="{.metadata.labels['external-ipv4']}")" + + if [ -z "${ENDPOINT}" ]; then + ENDPOINT="$(kubectl get node "${NODE_NAME}" -o jsonpath='{range .status.addresses[?(@.type=="ExternalIP")]}{.address}{end}')" + fi + + if [ -z "${ENDPOINT}" ]; then + echo "ERROR: node ${NODE_NAME} has no external-ipv4 label and no ExternalIP" + exit 1 + fi + + VALUE="${ENDPOINT}:${PORT}" + echo "Registering AmneziaWG endpoint: ${NODE_NAME} -> ${VALUE}" + + if kubectl get secret amneziawg-endpoints -n "${NAMESPACE}" >/dev/null 2>&1; then + kubectl patch secret amneziawg-endpoints -n "${NAMESPACE}" \ + --type merge -p "{\"stringData\":{\"${NODE_NAME}\":\"${VALUE}\"}}" + else + kubectl create secret generic amneziawg-endpoints -n "${NAMESPACE}" \ + --from-literal="${NODE_NAME}=${VALUE}" + fi + containers: + - name: amneziawg + image: amneziavpn/amneziawg-go:latest + imagePullPolicy: IfNotPresent + securityContext: + privileged: true + capabilities: + add: + - NET_ADMIN + - SYS_MODULE + command: + - /bin/bash + - /scripts/run.sh + ports: + - name: awg + containerPort: 5847 + protocol: UDP + readinessProbe: + exec: + command: + - /bin/bash + - -lc + - awg show awg0 >/dev/null 2>&1 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 3 + livenessProbe: + exec: + command: + - /bin/bash + - -lc + - awg show awg0 >/dev/null 2>&1 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 5 + failureThreshold: 3 + resources: + requests: + memory: "64Mi" + cpu: "50m" + limits: + memory: "256Mi" + cpu: "500m" + volumeMounts: + - name: server-config + mountPath: /etc/amnezia/server + readOnly: true + - name: client-config + mountPath: /etc/amnezia/clients + readOnly: true + - name: scripts + mountPath: /scripts + readOnly: true + - name: runtime-config + mountPath: /run/amnezia + - name: dev-net-tun + mountPath: /dev/net/tun + volumes: + - name: server-config + secret: + secretName: amneziawg-server + defaultMode: 0600 + items: + - key: awg0.conf + path: awg0.conf + - name: client-config + secret: + secretName: amneziawg-clients + optional: true + defaultMode: 0600 + - name: scripts + configMap: + name: amneziawg-scripts + defaultMode: 0755 + - name: runtime-config + emptyDir: {} + - name: dev-net-tun + hostPath: + path: /dev/net/tun + type: CharDevice diff --git a/k8s/apps/amnezia/external-secrets.yaml b/k8s/apps/amnezia/external-secrets.yaml new file mode 100644 index 0000000..cd4a546 --- /dev/null +++ b/k8s/apps/amnezia/external-secrets.yaml @@ -0,0 +1,50 @@ +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: amneziawg-server +spec: + target: + name: amneziawg-server + deletionPolicy: Delete + template: + type: Opaque + data: + server-public-key: |- + {{ .server_public_key }} + awg0.conf: |- + [Interface] + PrivateKey = {{ .server_private_key }} + Address = 10.8.1.1/24 + ListenPort = 5847 + MTU = 1376 + Jc = 4 + Jmin = 64 + Jmax = 128 + S1 = 15 + S2 = 18 + S3 = 20 + S4 = 23 + H1 = 1020325451 + H2 = 3288052141 + H3 = 1766607858 + H4 = 2528465083 + PostUp = /scripts/firewall-up.sh 5847 10.8.1.0/24 + PostDown = /scripts/firewall-down.sh 5847 10.8.1.0/24 + data: + - secretKey: server_private_key + sourceRef: + storeRef: + name: vaultwarden-login + kind: ClusterSecretStore + remoteRef: + key: 3092dc7c-41dd-461a-9f7a-377727f47e93 + property: fields[0].value + - secretKey: server_public_key + sourceRef: + storeRef: + name: vaultwarden-login + kind: ClusterSecretStore + remoteRef: + key: 3092dc7c-41dd-461a-9f7a-377727f47e93 + property: fields[1].value diff --git a/k8s/apps/amnezia/kustomization.yaml b/k8s/apps/amnezia/kustomization.yaml new file mode 100644 index 0000000..34e8eef --- /dev/null +++ b/k8s/apps/amnezia/kustomization.yaml @@ -0,0 +1,10 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - app.yaml + - namespace.yaml + - external-secrets.yaml + - configmap-scripts.yaml + - rbac.yaml + - daemonset.yaml diff --git a/k8s/apps/amnezia/namespace.yaml b/k8s/apps/amnezia/namespace.yaml new file mode 100644 index 0000000..38a93bc --- /dev/null +++ b/k8s/apps/amnezia/namespace.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: amnezia + labels: + pod-security.kubernetes.io/enforce: privileged + pod-security.kubernetes.io/audit: privileged + pod-security.kubernetes.io/warn: privileged diff --git a/k8s/apps/amnezia/rbac.yaml b/k8s/apps/amnezia/rbac.yaml new file mode 100644 index 0000000..c100746 --- /dev/null +++ b/k8s/apps/amnezia/rbac.yaml @@ -0,0 +1,58 @@ +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: amneziawg + labels: + app: amneziawg +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: amneziawg-node-reader + labels: + app: amneziawg +rules: + - apiGroups: [""] + resources: ["nodes"] + verbs: ["get", "list"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: amneziawg-node-reader + labels: + app: amneziawg +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: amneziawg-node-reader +subjects: + - kind: ServiceAccount + name: amneziawg + namespace: amnezia +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: amneziawg-endpoint-manager + labels: + app: amneziawg +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "create", "patch"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: amneziawg-endpoint-manager + labels: + app: amneziawg +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: amneziawg-endpoint-manager +subjects: + - kind: ServiceAccount + name: amneziawg