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