Skip to content

Commit

Permalink
docs: add Helm chart for VPN connectivity (#2577)
Browse files Browse the repository at this point in the history
Co-authored-by: 3u13r <[email protected]>
  • Loading branch information
burgerdev and 3u13r authored Nov 22, 2023
1 parent 968cdc1 commit 284c7e9
Show file tree
Hide file tree
Showing 16 changed files with 480 additions and 0 deletions.
23 changes: 23 additions & 0 deletions dev-docs/howto/vpn/.helmignore
Original file line number Diff line number Diff line change
@@ -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/
7 changes: 7 additions & 0 deletions dev-docs/howto/vpn/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v2
name: vpn
description: A VPN server for Constellation

type: application

version: 0.1.0
36 changes: 36 additions & 0 deletions dev-docs/howto/vpn/README.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions dev-docs/howto/vpn/files/strongswan/charon-logging.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
charon {
filelog {
stderr {
time_format = %b %e %T
ike_name = yes
default = 1
ike = 2
flush_line = yes
}
}
}
13 changes: 13 additions & 0 deletions dev-docs/howto/vpn/files/strongswan/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -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
38 changes: 38 additions & 0 deletions dev-docs/howto/vpn/files/tproxy-setup.sh
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions dev-docs/howto/vpn/files/wireguard-setup.sh
Original file line number Diff line number Diff line change
@@ -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
40 changes: 40 additions & 0 deletions dev-docs/howto/vpn/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
27 changes: 27 additions & 0 deletions dev-docs/howto/vpn/templates/configmaps.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
21 changes: 21 additions & 0 deletions dev-docs/howto/vpn/templates/secrets.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
26 changes: 26 additions & 0 deletions dev-docs/howto/vpn/templates/service.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
26 changes: 26 additions & 0 deletions dev-docs/howto/vpn/templates/strongswan-secret.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
78 changes: 78 additions & 0 deletions dev-docs/howto/vpn/templates/strongswan-statefulset.yaml
Original file line number Diff line number Diff line change
@@ -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 }}
14 changes: 14 additions & 0 deletions dev-docs/howto/vpn/templates/wireguard-secret.tpl
Original file line number Diff line number Diff line change
@@ -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 }}
Loading

0 comments on commit 284c7e9

Please sign in to comment.