diff --git a/add-ons/authentik/blueprints.yaml b/add-ons/authentik/blueprints.yaml
index fe2604b..e04fe81 100644
--- a/add-ons/authentik/blueprints.yaml
+++ b/add-ons/authentik/blueprints.yaml
@@ -99,3 +99,42 @@ data:
name: Grafana
provider: !KeyOf grafana-provider
meta_launch_url: https://grafana.whyredfire.dev
+ openvpn-admin.yaml: |
+ version: 1
+ metadata:
+ name: OpenVPN admin
+ entries:
+ - model: authentik_providers_proxy.proxyprovider
+ id: openvpn-admin-provider
+ identifiers:
+ name: openvpn-admin
+ attrs:
+ authorization_flow: !Find [authentik_flows.flow, [slug, default-provider-authorization-implicit-consent]]
+ invalidation_flow: !Find [authentik_flows.flow, [slug, default-provider-invalidation-flow]]
+ external_host: https://openvpn-admin.whyredfire.dev
+ mode: forward_single
+ - model: authentik_core.application
+ id: openvpn-admin
+ identifiers:
+ slug: openvpn-admin
+ attrs:
+ name: OpenVPN Admin
+ provider: !KeyOf openvpn-admin-provider
+ meta_launch_url: https://openvpn-admin.whyredfire.dev
+ # Issuing VPN certificates is admin-only.
+ - model: authentik_policies.policybinding
+ identifiers:
+ order: 0
+ target: !KeyOf openvpn-admin
+ attrs:
+ group: !Find [authentik_core.group, [name, admins]]
+ # This replaces the embedded outpost's provider list, so it also names the
+ # providers created in the UI.
+ - model: authentik_outposts.outpost
+ identifiers:
+ managed: goauthentik.io/outposts/embedded
+ attrs:
+ providers:
+ - !Find [authentik_providers_proxy.proxyprovider, [name, garage-ui]]
+ - !Find [authentik_providers_proxy.proxyprovider, [name, qbittorrent]]
+ - !KeyOf openvpn-admin-provider
diff --git a/add-ons/authentik/referencegrant.yaml b/add-ons/authentik/referencegrant.yaml
index bf1d8a0..a2d4553 100644
--- a/add-ons/authentik/referencegrant.yaml
+++ b/add-ons/authentik/referencegrant.yaml
@@ -8,6 +8,9 @@ spec:
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: garage
+ - group: gateway.networking.k8s.io
+ kind: HTTPRoute
+ namespace: openvpn
- group: gateway.networking.k8s.io
kind: HTTPRoute
namespace: qbit
diff --git a/apps/openvpn/configmap.yaml b/apps/openvpn/configmap.yaml
new file mode 100644
index 0000000..8df3b7d
--- /dev/null
+++ b/apps/openvpn/configmap.yaml
@@ -0,0 +1,97 @@
+apiVersion: v1
+data:
+ client.conf.tpl: |
+ client
+ dev tun
+ {{- range $server := .Hosts }}
+ remote {{ $server.Host }} {{ $server.Port }} {{ $server.Protocol }}
+ {{- end }}
+ nobind
+ persist-key
+ persist-tun
+ remote-cert-tls server
+ tls-version-min 1.2
+ verb 3
+
+
+ {{ .CA -}}
+
+
+ {{ .Cert -}}
+
+
+ {{ .Key -}}
+
+
+ {{ .TLS -}}
+
+ entrypoint.sh: |
+ #!/bin/sh
+ set -eu
+
+ # These rules live in the pod's network namespace, not the node's. VPN clients
+ # may reach the home LAN, but not this pod (ovpn-admin has no auth of its own),
+ # the pod network or the service network.
+ iptables -F INPUT
+ iptables -F FORWARD
+ iptables -t nat -F POSTROUTING
+ iptables -A INPUT -i tun0 -j DROP
+ iptables -A FORWARD -i tun0 -d "$CLUSTER_CIDR" -j DROP
+ iptables -A FORWARD -i tun0 -d "$SERVICE_CIDR" -j DROP
+ iptables -t nat -A POSTROUTING -s "$VPN_NETWORK" -o eth0 -j MASQUERADE
+
+ if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != 1 ]; then
+ echo "net.ipv4.ip_forward is disabled in the pod network namespace" >&2
+ exit 1
+ fi
+
+ mkdir -p /dev/net
+ [ -c /dev/net/tun ] || mknod /dev/net/tun c 10 200
+
+ for f in ca.crt issued/server.crt private/server.key ta.key crl.pem; do
+ until [ -f "/etc/openvpn/certs/pki/$f" ]; do
+ echo "waiting for $f from ovpn-admin"
+ sleep 2
+ done
+ done
+
+ exec openvpn --config /etc/openvpn/openvpn.conf
+ openvpn.conf: |
+ # After editing this ConfigMap, bump openvpn.whyredfire.dev/config-revision
+ # on the Deployment to restart it.
+ port 1194
+ proto udp
+ dev tun0
+ topology subnet
+ server 10.8.0.0 255.255.255.0
+ client-config-dir /etc/openvpn/ccd
+ management 127.0.0.1 8989
+
+ # Written by ovpn-admin from the PKI it keeps in Kubernetes Secrets.
+ ca /etc/openvpn/certs/pki/ca.crt
+ cert /etc/openvpn/certs/pki/issued/server.crt
+ key /etc/openvpn/certs/pki/private/server.key
+ crl-verify /etc/openvpn/certs/pki/crl.pem
+ tls-crypt /etc/openvpn/certs/pki/ta.key
+ dh none
+ tls-version-min 1.2
+ remote-cert-tls client
+
+ # Split tunnel: only the home LAN goes through the VPN.
+ push "route 192.168.2.0 255.255.255.0"
+ push "dhcp-option DNS 192.168.2.1"
+
+ # DCO would need the ovpn kernel module on the node; plain tun keeps the pod self-contained.
+ disable-dco
+ keepalive 10 60
+ persist-key
+ persist-tun
+ explicit-exit-notify 1
+ user nobody
+ group nogroup
+ status /tmp/openvpn-status.log
+ verb 3
+kind: ConfigMap
+metadata:
+ name: openvpn
+ namespace: openvpn
diff --git a/apps/openvpn/deployment.yaml b/apps/openvpn/deployment.yaml
new file mode 100644
index 0000000..cc93a1a
--- /dev/null
+++ b/apps/openvpn/deployment.yaml
@@ -0,0 +1,138 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: openvpn
+ namespace: openvpn
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: openvpn
+ strategy:
+ type: Recreate
+ template:
+ metadata:
+ annotations:
+ openvpn.whyredfire.dev/config-revision: "1"
+ labels:
+ app.kubernetes.io/name: openvpn
+ spec:
+ containers:
+ - args:
+ - |
+ # The sync API serves the PKI to replica servers, which this
+ # setup doesn't use, so lock it with a throwaway token.
+ exec /app/ovpn-admin \
+ --storage.backend=kubernetes.secrets \
+ --listen.port=8080 \
+ --mgmt=main=127.0.0.1:8989 \
+ --ccd \
+ --ccd.path=/mnt/ccd \
+ --easyrsa.path=/mnt/certs \
+ --ovpn.network=10.8.0.0/24 \
+ --ovpn.server=openvpn.whyredfire.dev:27299:udp \
+ --templates.clientconfig-path=/templates/client.conf.tpl \
+ --master.sync-token="$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n')"
+ command:
+ - /bin/sh
+ - -c
+ image: ghcr.io/palark/ovpn-admin/ovpn-admin:master@sha256:8487aec078954ba682fb22cd2882ec3e4faaa03a47cc3e06f786b3ec493ded79
+ name: ovpn-admin
+ ports:
+ - containerPort: 8080
+ name: http
+ readinessProbe:
+ httpGet:
+ path: /ping
+ port: http
+ resources:
+ limits:
+ cpu: 500m
+ memory: 128Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /mnt/certs
+ name: certs
+ - mountPath: /mnt/ccd
+ name: ccd
+ - mountPath: /templates/client.conf.tpl
+ name: config
+ readOnly: true
+ subPath: client.conf.tpl
+ - command:
+ - /entrypoint.sh
+ env:
+ - name: CLUSTER_CIDR
+ value: 10.42.0.0/16
+ - name: SERVICE_CIDR
+ value: 10.43.0.0/16
+ - name: VPN_NETWORK
+ value: 10.8.0.0/24
+ image: ghcr.io/palark/ovpn-admin/openvpn:master@sha256:130657e1938675f9492dcb554fdf63357eaddad90a1514c73db9a433e3de77b6
+ name: openvpn
+ ports:
+ - containerPort: 1194
+ name: openvpn
+ protocol: UDP
+ resources:
+ limits:
+ cpu: "1"
+ memory: 128Mi
+ requests:
+ cpu: 10m
+ memory: 32Mi
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ add:
+ - MKNOD
+ - NET_ADMIN
+ - NET_RAW
+ - SETGID
+ - SETUID
+ drop:
+ - ALL
+ volumeMounts:
+ - mountPath: /dev/net
+ name: dev-net
+ - mountPath: /etc/openvpn/certs
+ name: certs
+ readOnly: true
+ - mountPath: /etc/openvpn/ccd
+ name: ccd
+ readOnly: true
+ - mountPath: /etc/openvpn/openvpn.conf
+ name: config
+ readOnly: true
+ subPath: openvpn.conf
+ - mountPath: /entrypoint.sh
+ name: entrypoint
+ readOnly: true
+ subPath: entrypoint.sh
+ dnsConfig:
+ nameservers:
+ - 192.168.2.1
+ dnsPolicy: None
+ serviceAccountName: openvpn
+ volumes:
+ - emptyDir:
+ medium: Memory
+ name: certs
+ - emptyDir: {}
+ name: ccd
+ - emptyDir: {}
+ name: dev-net
+ - configMap:
+ name: openvpn
+ name: config
+ - configMap:
+ defaultMode: 493
+ name: openvpn
+ name: entrypoint
diff --git a/apps/openvpn/httproute.yaml b/apps/openvpn/httproute.yaml
new file mode 100644
index 0000000..52abe84
--- /dev/null
+++ b/apps/openvpn/httproute.yaml
@@ -0,0 +1,37 @@
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+ annotations:
+ argocd.argoproj.io/sync-wave: "2"
+ name: openvpn-admin
+ namespace: openvpn
+spec:
+ hostnames:
+ - openvpn-admin.whyredfire.dev
+ parentRefs:
+ - kind: Gateway
+ name: traefik-gateway
+ namespace: traefik
+ sectionName: websecure
+ rules:
+ - backendRefs:
+ - name: authentik-server
+ namespace: authentik
+ port: 80
+ matches:
+ - path:
+ type: PathPrefix
+ value: /outpost.goauthentik.io
+ - backendRefs:
+ - name: openvpn-admin
+ port: 8080
+ filters:
+ - extensionRef:
+ group: traefik.io
+ kind: Middleware
+ name: authentik
+ type: ExtensionRef
+ matches:
+ - path:
+ type: PathPrefix
+ value: /
diff --git a/apps/openvpn/kustomization.yaml b/apps/openvpn/kustomization.yaml
new file mode 100644
index 0000000..3b3ab68
--- /dev/null
+++ b/apps/openvpn/kustomization.yaml
@@ -0,0 +1,13 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+resources:
+ - serviceaccount.yaml
+ - role.yaml
+ - rolebinding.yaml
+ - configmap.yaml
+ - deployment.yaml
+ - service.yaml
+ - service-admin.yaml
+ - networkpolicy.yaml
+ - middleware.yaml
+ - httproute.yaml
diff --git a/apps/openvpn/middleware.yaml b/apps/openvpn/middleware.yaml
new file mode 100644
index 0000000..75e81d7
--- /dev/null
+++ b/apps/openvpn/middleware.yaml
@@ -0,0 +1,17 @@
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+ annotations:
+ argocd.argoproj.io/sync-wave: "1"
+ name: authentik
+ namespace: openvpn
+spec:
+ forwardAuth:
+ address: http://authentik-server.authentik.svc.cluster.local/outpost.goauthentik.io/auth/traefik
+ authResponseHeaders:
+ - X-authentik-username
+ - X-authentik-groups
+ - X-authentik-email
+ - X-authentik-name
+ - X-authentik-uid
+ trustForwardHeader: true
diff --git a/apps/openvpn/networkpolicy.yaml b/apps/openvpn/networkpolicy.yaml
new file mode 100644
index 0000000..f08d460
--- /dev/null
+++ b/apps/openvpn/networkpolicy.yaml
@@ -0,0 +1,22 @@
+apiVersion: networking.k8s.io/v1
+kind: NetworkPolicy
+metadata:
+ name: openvpn
+ namespace: openvpn
+spec:
+ ingress:
+ - ports:
+ - port: 1194
+ protocol: UDP
+ - from:
+ - namespaceSelector:
+ matchLabels:
+ kubernetes.io/metadata.name: traefik
+ ports:
+ - port: 8080
+ protocol: TCP
+ podSelector:
+ matchLabels:
+ app.kubernetes.io/name: openvpn
+ policyTypes:
+ - Ingress
diff --git a/apps/openvpn/role.yaml b/apps/openvpn/role.yaml
new file mode 100644
index 0000000..0c44c9e
--- /dev/null
+++ b/apps/openvpn/role.yaml
@@ -0,0 +1,15 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: openvpn
+ namespace: openvpn
+rules:
+ - apiGroups:
+ - ""
+ resources:
+ - secrets
+ verbs:
+ - create
+ - get
+ - list
+ - update
diff --git a/apps/openvpn/rolebinding.yaml b/apps/openvpn/rolebinding.yaml
new file mode 100644
index 0000000..ebe79fc
--- /dev/null
+++ b/apps/openvpn/rolebinding.yaml
@@ -0,0 +1,13 @@
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: openvpn
+ namespace: openvpn
+roleRef:
+ apiGroup: rbac.authorization.k8s.io
+ kind: Role
+ name: openvpn
+subjects:
+ - kind: ServiceAccount
+ name: openvpn
+ namespace: openvpn
diff --git a/apps/openvpn/service-admin.yaml b/apps/openvpn/service-admin.yaml
new file mode 100644
index 0000000..670c6e2
--- /dev/null
+++ b/apps/openvpn/service-admin.yaml
@@ -0,0 +1,12 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: openvpn-admin
+ namespace: openvpn
+spec:
+ ports:
+ - name: http
+ port: 8080
+ targetPort: http
+ selector:
+ app.kubernetes.io/name: openvpn
diff --git a/apps/openvpn/service.yaml b/apps/openvpn/service.yaml
new file mode 100644
index 0000000..387023e
--- /dev/null
+++ b/apps/openvpn/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: openvpn
+ namespace: openvpn
+spec:
+ externalIPs:
+ - 192.168.2.5
+ ports:
+ - name: openvpn
+ port: 51820
+ protocol: UDP
+ targetPort: openvpn
+ selector:
+ app.kubernetes.io/name: openvpn
diff --git a/apps/openvpn/serviceaccount.yaml b/apps/openvpn/serviceaccount.yaml
new file mode 100644
index 0000000..02717b6
--- /dev/null
+++ b/apps/openvpn/serviceaccount.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: openvpn
+ namespace: openvpn
diff --git a/argocd-apps/apps/kustomization.yaml b/argocd-apps/apps/kustomization.yaml
index 523b6f9..e5edfc1 100644
--- a/argocd-apps/apps/kustomization.yaml
+++ b/argocd-apps/apps/kustomization.yaml
@@ -7,6 +7,7 @@ resources:
- garage-ui.yaml
- jellyfin.yaml
- legislative-summarization.yaml
+ - openvpn.yaml
- qbit.yaml
- redoost.yaml
- rustbin.yaml
diff --git a/argocd-apps/apps/openvpn.yaml b/argocd-apps/apps/openvpn.yaml
new file mode 100644
index 0000000..1d44c98
--- /dev/null
+++ b/argocd-apps/apps/openvpn.yaml
@@ -0,0 +1,22 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ annotations:
+ argocd.argoproj.io/sync-wave: "3"
+ name: openvpn
+ namespace: argocd
+spec:
+ destination:
+ namespace: openvpn
+ server: https://kubernetes.default.svc
+ project: default
+ source:
+ path: apps/openvpn
+ repoURL: https://gitea.whyredfire.dev/whyredfire/gitops.git
+ targetRevision: main
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true