docs: add Helm chart for VPN connectivity (#2577)

Co-authored-by: 3u13r <lc@edgeless.systems>
This commit is contained in:
Markus Rudy 2023-11-22 15:08:11 +01:00 committed by GitHub
parent 968cdc1a38
commit 284c7e99d1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 480 additions and 0 deletions

View File

@ -0,0 +1,23 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@ -0,0 +1,7 @@
apiVersion: v2
name: vpn
description: A VPN server for Constellation
type: application
version: 0.1.0

View File

@ -0,0 +1,36 @@
# Constellation VPN
This Helm chart deploys a VPN server to your Constellation cluster.
## Installation
1. Create and populate the configuration.
```sh
helm inspect values . >config.yaml
```
2. Install the Helm chart.
```sh
helm install -f config.yaml vpn .
```
3. Follow the post-installation instructions displayed by the CLI.
## Architecture
The VPN server is deployed as a `StatefulSet` to the cluster. It hosts the VPN frontend component, which is responsible for relaying traffic between the pod and the on-prem network, and the routing components that provide access to Constellation resources. The frontend supports IPSec and Wireguard.
The VPN frontend is exposed with a public LoadBalancer to be accessible from the on-prem network. Traffic that reaches the VPN server pod is split into two categories: pod IPs and service IPs.
The pod IP range is NATed with an iptables rule. On-prem worklaods can establish connections to a pod IP, but the Constellation workloads will see the client IP translated to that of the VPN frontend pod.
The service IP range is handed to a transparent proxy running in the VPN frontend pod, which relays the connection to a backend pod. This is necessary because of the load-balancing mechanism of Cilium, which assumes service IP traffic to originate from the Constellation cluster itself. As for pod IP ranges, Constellation pods will only see the translated client address.
## Limitations
* Service IPs need to be proxied by the VPN frontend pod. This is a single point of failure, and it may become a bottleneck.
* IPs are NATed, so the Constellation pods won't see the real on-prem IPs.
* NetworkPolicy can't be applied selectively to the on-prem ranges.
* No connectivity from Constellation to on-prem workloads.

View File

@ -0,0 +1,11 @@
charon {
filelog {
stderr {
time_format = %b %e %T
ike_name = yes
default = 1
ike = 2
flush_line = yes
}
}
}

View File

@ -0,0 +1,13 @@
#!/bin/sh
# The charon binary is not included in the PATH generated by nixery.dev, find it manually.
charon="$(dirname "$(readlink -f "$(command -v charon-systemd)")")/../libexec/ipsec/charon"
"${charon}" &
while ! swanctl --stats > /dev/null 2> /dev/null; do
sleep 1
done
swanctl --load-all
wait

View File

@ -0,0 +1,38 @@
#!/bin/sh
set -eu
### Pod IPs ###
# Pod IPs are just NATed.
iptables -t nat -N VPN_POST || iptables -t nat -F VPN_POST
for cidr in ${VPN_PEER_CIDRS}; do
iptables -t nat -A VPN_POST -s "${cidr}" -d "${VPN_POD_CIDR}" -j MASQUERADE
done
iptables -t nat -C POSTROUTING -j VPN_POST || iptables -t nat -A POSTROUTING -j VPN_POST
### Service IPs ###
# Service IPs need to be connected to locally to trigger the cgroup connect hook, thus we send them to the transparent proxy.
# Packets with mark 1 are for tproxy and need to be delivered locally.
# For more information see: https://www.kernel.org/doc/Documentation/networking/tproxy.txt
pref=42
table=42
mark=0x1/0x1
ip rule add pref "${pref}" fwmark "${mark}" lookup "${table}"
ip route replace local 0.0.0.0/0 dev lo table "${table}"
iptables -t mangle -N VPN_PRE || iptables -t mangle -F VPN_PRE
for cidr in ${VPN_PEER_CIDRS}; do
for proto in tcp udp; do
iptables -t mangle -A VPN_PRE -p "${proto}" -s "${cidr}" -d "${VPN_SERVICE_CIDR}" \
-j TPROXY --tproxy-mark "${mark}" --on-port 61001
done
done
iptables -t mangle -C PREROUTING -j VPN_PRE || iptables -t mangle -A PREROUTING -j VPN_PRE

View File

@ -0,0 +1,13 @@
#!/bin/sh
set -eu
dev=vpn_wg0
ip link add dev "${dev}" type wireguard
wg setconf "${dev}" /etc/wireguard/wg.conf
ip link set dev "${dev}" up
for cidr in ${VPN_PEER_CIDRS}; do
ip route replace "${cidr}" dev "${dev}"
done

View File

@ -0,0 +1,40 @@
{{- define "..name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 42 | trimSuffix "-" }}
{{- end }}
{{- define "..fullname" -}}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 42 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 42 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "..chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 42 | trimSuffix "-" }}
{{- end }}
{{- define "..labels" -}}
helm.sh/chart: {{ include "..chart" . }}
{{ include "..selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "..selectorLabels" -}}
app.kubernetes.io/name: {{ include "..name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{- define "..commonEnv" -}}
- name: VPN_PEER_CIDRS
value: {{ join " " .Values.peerCIDRs | quote }}
- name: VPN_POD_CIDR
value: {{ .Values.podCIDR | quote }}
- name: VPN_SERVICE_CIDR
value: {{ .Values.serviceCIDR | quote }}
{{- end }}

View File

@ -0,0 +1,27 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "..fullname" . }}-tproxy
labels: {{- include "..labels" . | nindent 4 }}
data:
{{ (.Files.Glob "files/tproxy-setup.sh").AsConfig | indent 2 }}
---
{{- if .Values.wireguard.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "..fullname" . }}-wg
labels: {{- include "..labels" . | nindent 4 }}
data:
{{ (.Files.Glob "files/wireguard-setup.sh").AsConfig | indent 2 }}
{{- end }}
---
{{ if .Values.ipsec.enabled }}
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "..fullname" . }}-strongswan
labels: {{- include "..labels" . | nindent 4 }}
data:
{{ (.Files.Glob "files/strongswan/*").AsConfig | indent 2 }}
{{- end }}

View File

@ -0,0 +1,21 @@
{{- if .Values.wireguard.enabled }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "..fullname" . }}-wg
labels:
{{- include "..labels" . | nindent 4 }}
data:
wg.conf: {{ include "wireguard.conf" . | b64enc }}
{{- end }}
---
{{ if .Values.ipsec.enabled }}
apiVersion: v1
kind: Secret
metadata:
name: {{ include "..fullname" . }}-strongswan
labels:
{{- include "..labels" . | nindent 4 }}
data:
swanctl.conf: {{ include "strongswan.swanctl-conf" . | b64enc }}
{{- end }}

View File

@ -0,0 +1,26 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "..fullname" . }}-lb
labels:
{{- include "..labels" . | nindent 4 }}
spec:
type: LoadBalancer
selector:
{{- include "..selectorLabels" . | nindent 4 }}
component: frontend
externalTrafficPolicy: Local
ports:
{{- if .Values.ipsec.enabled }}
- name: isakmp
protocol: UDP
port: 500
- name: ipsec-nat-t
protocol: UDP
port: 4500
{{- end }}
{{- if .Values.wireguard.enabled }}
- name: wg
protocol: UDP
port: {{ .Values.wireguard.port }}
{{- end }}

View File

@ -0,0 +1,26 @@
{{- define "strongswan.swanctl-conf" }}
connections {
net-net {
remote_addrs = {{ .Values.ipsec.peer }}
local {
auth = psk
}
remote {
auth = psk
}
children {
net-net {
local_ts = {{ .Values.podCIDR }},{{ .Values.serviceCIDR }}
remote_ts = {{ join "," .Values.peerCIDRs }}
start_action = trap
}
}
}
}
secrets {
ike {
secret = {{ quote .Values.ipsec.psk }}
}
}
{{- end }}

View File

@ -0,0 +1,78 @@
{{ if .Values.ipsec.enabled -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "..fullname" . }}-frontend
labels: {{- include "..labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "..selectorLabels" . | nindent 6 }}
component: frontend
template:
metadata:
labels:
{{- include "..selectorLabels" . | nindent 8 }}
component: frontend
spec:
hostNetwork: false
initContainers:
- name: tproxy-setup
image: nixery.dev/busybox/iptables
command: ["/bin/sh", "-x", "/entrypoint.sh"]
env: {{- include "..commonEnv" . | nindent 10 }}
securityContext:
capabilities:
add: ["NET_ADMIN"]
volumeMounts:
- name: tproxy-setup
mountPath: "/entrypoint.sh"
subPath: "tproxy-setup.sh"
readOnly: true
containers:
- name: tproxy
# Image source: github.com/burgerdev/go-tproxy
image: ghcr.io/burgerdev/go-tproxy:latest
command: ["/tproxy", "--port=61001", "--nat=true"]
securityContext:
capabilities:
add: ["NET_RAW"]
- name: strongswan
image: "nixery.dev/shell/strongswan"
command: ["/bin/sh", "-x", "/entrypoint.sh"]
securityContext:
capabilities:
add: ["NET_ADMIN"]
volumeMounts:
- name: strongswan
mountPath: "/entrypoint.sh"
subPath: "entrypoint.sh"
readOnly: true
- name: strongswan
mountPath: "/etc/strongswan.d/charon-logging.conf"
subPath: "charon-logging.conf"
readOnly: true
- name: strongswan
mountPath: "/etc/swanctl/swanctl.conf"
subPath: "swanctl.conf"
readOnly: true
volumes:
- name: tproxy-setup
configMap:
name: {{ include "..fullname" . }}-tproxy
- name: strongswan
projected:
sources:
- secret:
name: {{ include "..fullname" . }}-strongswan
items:
- key: swanctl.conf
path: swanctl.conf
- configMap:
name: {{ include "..fullname" . }}-strongswan
items:
- key: entrypoint.sh
path: entrypoint.sh
- key: charon-logging.conf
path: charon-logging.conf
{{- end }}

View File

@ -0,0 +1,14 @@
{{- define "wireguard.conf" }}
[Interface]
ListenPort = {{ .Values.wireguard.port }}
PrivateKey = {{ .Values.wireguard.private_key }}
[Peer]
PublicKey = {{ .Values.wireguard.peer_key }}
AllowedIPs = {{ join "," .Values.peerCIDRs }}
{{- if .Values.wireguard.endpoint }}
Endpoint = {{- .Values.wireguard.endpoint }}
{{- end }}
{{- if .Values.wireguard.keepAlive }}
PersistentKeepalive = {{- .Values.wireguard.keepAlive }}
{{- end }}
{{ end }}

View File

@ -0,0 +1,68 @@
{{ if .Values.wireguard.enabled -}}
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ include "..fullname" . }}-frontend
labels: {{- include "..labels" . | nindent 4 }}
spec:
selector:
matchLabels:
{{- include "..selectorLabels" . | nindent 6 }}
component: frontend
template:
metadata:
labels:
{{- include "..selectorLabels" . | nindent 8 }}
component: frontend
spec:
hostNetwork: false
initContainers:
- name: tproxy-setup
image: nixery.dev/busybox/iptables
command: ["/bin/sh", "-x", "/entrypoint.sh"]
env: {{- include "..commonEnv" . | nindent 10 }}
securityContext:
capabilities:
add: ["NET_ADMIN"]
volumeMounts:
- name: tproxy-setup
mountPath: "/entrypoint.sh"
subPath: "tproxy-setup.sh"
readOnly: true
- name: wg-setup
image: "nixery.dev/busybox/wireguard-tools"
command: ["/bin/sh", "-x", "/etc/wireguard/wireguard-setup.sh"]
env: {{- include "..commonEnv" . | nindent 10 }}
securityContext:
capabilities:
add: ["NET_ADMIN"]
volumeMounts:
- name: wireguard
mountPath: "/etc/wireguard"
readOnly: true
containers:
- name: tproxy
# Image source: github.com/burgerdev/go-tproxy
image: ghcr.io/burgerdev/go-tproxy:latest
command: ["/tproxy", "--port=61001", "--nat=true"]
securityContext:
capabilities:
add: ["NET_RAW"]
volumes:
- name: tproxy-setup
configMap:
name: {{ include "..fullname" . }}-tproxy
- name: wireguard
projected:
sources:
- secret:
name: {{ include "..fullname" . }}-wg
items:
- key: wg.conf
path: wg.conf
- configMap:
name: {{ include "..fullname" . }}-wg
items:
- key: wireguard-setup.sh
path: wireguard-setup.sh
{{- end }}

View File

@ -0,0 +1,39 @@
# Constellation Pod IP range to expose via VPN. The default is for GCP.
podCIDR: "10.10.0.0/16"
# Constellation Service IPs to expose via VPN. The default is for GCP.
serviceCIDR: "10.96.0.0/12"
# on-prem IP ranges to expose to Constellation. Must contain at least one CIDR.
peerCIDRs: []
# The sections below configure the VPN connectivity to the Constellation
# cluster. Exactly one `enabled` must be set to true.
# IPSec configuration
ipsec:
enabled: false
# pre-shared key used for authentication
psk: ""
# Address of the peer's gateway router.
peer: ""
# Wireguard configuration
wireguard:
enabled: false
# If Wireguard is enabled, these fields for the Constellation side must be populated.
private_key: ""
peer_key: ""
# Listening port of the Constellation Wireguard.
port: 51820
# Optional host:port of the on-prem Wireguard.
endpoint: ""
# Optional interval for keep-alive packets in seconds. Setting this helps the on-prem server to
# discover a restarted Constellation VPN frontend.
keepAlive: ""