From 76a76132bdd050e35361a1bd546aec0ccdc1452d Mon Sep 17 00:00:00 2001 From: niqdev Date: Sat, 21 Oct 2023 17:49:51 +0100 Subject: [PATCH] fix remote kube vpn --- README.md | 58 ++++++++++++------ internal/command/config/model.go | 5 +- internal/command/config/model_test.go | 1 + pkg/common/docker/client.go | 5 +- pkg/common/kubernetes/builder.go | 79 ++++++++++++++---------- pkg/common/kubernetes/builder_test.go | 87 ++++++++++++++++++++++----- pkg/common/kubernetes/client.go | 2 +- pkg/common/model/constant.go | 9 +-- pkg/common/model/types.go | 1 + 9 files changed, 174 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 7f910e5..8d1d204 100644 --- a/README.md +++ b/README.md @@ -69,13 +69,13 @@ hckctl box start vulnerable/owasp-juice-shop > TODO video -Access your target from a managed [`lab`](https://github.com/hckops/megalopolis/tree/main/lab) +Access your target from a managed [`lab`](https://github.com/hckops/megalopolis/tree/main/lab) to * tunnel multiple vpn connections through a high-available ssh proxy * expose public endpoints * pre-mount saved `dumps` (git, s3) * load secrets from a vault * save/restore workdir snapshots -* deploy custom labs +* deploy private templates ```bash hckctl lab ctf-linux ``` @@ -100,6 +100,9 @@ hckctl task nmap --command full --input address=127.0.0.1 --input port=80 # invokes it with custom arguments hckctl task nuclei --inline -- -u https://example.com +go run internal/main.go task nmap --network-vpn htb --provider kube --inline -- nmap 10.10.10.3 -sC -sV +go run internal/main.go task nmap --network-vpn htb --provider kube --command full --input address=10.10.10.3 + # monitors the logs tail -F ${HOME}/.local/state/hck/task/log/task-* ``` @@ -207,45 +210,55 @@ network: Follow the official [instructions](https://docs.docker.com/engine/install) to install Docker Engine. The fastest way to get started is with the [convenience script](https://get.docker.com) ```bash -# download and run script +# downloads and runs script curl -fsSL https://get.docker.com -o get-docker.sh ./sudo sh get-docker.sh ``` +Recommended tool to watch the container [lazydocker](https://github.com/jesseduffield/lazydocker) + ### Kubernetes -Use [minikube](https://minikube.sigs.k8s.io) or [kind](https://kind.sigs.k8s.io) to setup a local cluster +If you are looking for a simple and cheap way to get started with a remote cluster consider using [kube-template](https://github.com/hckops/kube-template) on [DigitalOcean](https://www.digitalocean.com/products/kubernetes) ```bash provider: kube: - # by default uses "~/.kube/config" + configPath: "/PATH/TO/kube-template/clusters/do-template-kubeconfig.yaml" +``` + +Use [minikube](https://minikube.sigs.k8s.io), [kind](https://kind.sigs.k8s.io) or [k3s](https://k3s.io) to setup a local cluster +```bash +provider: + kube: + # absolute path, empty by default uses "${HOME}/.kube/config" configPath: "" namespace: hckops ``` -Make sure you disable IPv6 in your cluster to use the `--network-vpn` flag +Make sure you disable IPv6 in your *local* cluster to use the `--network-vpn` flag and set `--embed-certs` if you need to access the cluster using the dev tools ```bash -# set --embed-certs to run "hckops/kube-base" -minikube start --embed-certs --extra-config="kubelet.allowed-unsafe-sysctls=net.ipv6.conf.all.disable_ipv6" +# starts local cluster +minikube start --embed-certs \ + --extra-config="kubelet.allowed-unsafe-sysctls=net.ipv6.conf.all.disable_ipv6" + +# runs with temporary privileges to connect to a vpn +env HCK_CONFIG_NETWORK.PRIVILEGED=true hckctl box alpine --provider kube --network-vpn htb + +network: + # default is false, required only for local clusters + privileged: true ``` -Useful dev tools +Useful dev tools, see [`hckops/kube-base`](https://github.com/hckops/actions/blob/main/docker/Dockerfile.base) ```bash # starts tmp container -docker run --rm --name hck-tmp --network host -it \ +docker run --rm --name hck-tmp-local --network host -it \ -v ${HOME}/.kube/config:/root/.kube/config hckops/kube-base # watches pods kubectl klock -n hckops pods ``` -If you are looking for a simple way to get started with a remote cluster consider using [kube-template](https://github.com/hckops/kube-template) -```bash -provider: - kube: - configPath: "~/PATH/TO/kube-template/clusters/do-template-kubeconfig.yaml" -``` - ### Cloud Access to the platform is limited and in ***private preview***. If you are interested, please leave a comment or a :thumbsup: to this [issue](https://github.com/hckops/hckctl/issues/104) and we'll reach out with more details @@ -272,6 +285,9 @@ HCKCTL_VERSION=0.12.0 curl -sSL https://github.com/hckops/hckctl/releases/latest/download/hckctl-${HCKCTL_VERSION}-linux-x86_64.tar.gz | \ sudo tar -xzf - -C /usr/local/bin +# verify +hckctl version + # uninstall sudo rm /usr/local/bin/hckctl ``` @@ -318,10 +334,14 @@ Credit should go to all the authors and maintainers for their open source tools, diff --git a/internal/command/config/model.go b/internal/command/config/model.go index 136a563..78c52d0 100644 --- a/internal/command/config/model.go +++ b/internal/command/config/model.go @@ -94,7 +94,8 @@ func (c *CloudConfig) ToCloudOptions(version string) *commonModel.CloudOptions { } type NetworkConfig struct { - Vpn []VpnConfig `yaml:"vpn"` + Privileged bool `yaml:"privileged"` + Vpn []VpnConfig `yaml:"vpn"` } type VpnConfig struct { @@ -111,6 +112,7 @@ func (c *NetworkConfig) VpnNetworks() map[string]commonModel.NetworkVpnInfo { Name: network.Name, LocalPath: network.Path, ConfigValue: configFile, + Privileged: c.Privileged, } } } @@ -186,6 +188,7 @@ func newConfig(opts *configOptions) *ConfigV1 { }, }, Network: NetworkConfig{ + Privileged: false, Vpn: []VpnConfig{ {Name: common.DefaultVpnName, Path: "/path/to/client.ovpn"}, }, diff --git a/internal/command/config/model_test.go b/internal/command/config/model_test.go index 38b6c1f..d891f7e 100644 --- a/internal/command/config/model_test.go +++ b/internal/command/config/model_test.go @@ -41,6 +41,7 @@ func TestNewConfig(t *testing.T) { }, }, Network: NetworkConfig{ + Privileged: false, Vpn: []VpnConfig{ {Name: "default", Path: "/path/to/client.ovpn"}, }, diff --git a/pkg/common/docker/client.go b/pkg/common/docker/client.go index 95026f4..b9d5cea 100644 --- a/pkg/common/docker/client.go +++ b/pkg/common/docker/client.go @@ -114,8 +114,9 @@ func (common *DockerCommonClient) SidecarVpnInject(opts *commonModel.SidecarVpnI // sidecarName containerName := buildSidecarVpnName(opts.Name) - // constants - imageName := commonModel.SidecarVpnImageName + // ignore opts.NetworkVpn.Privileged locally + imageName := commonModel.SidecarVpnPrivilegedImageName + // base directory "/usr/share" must exist vpnConfigPath := "/usr/share/client.ovpn" diff --git a/pkg/common/kubernetes/builder.go b/pkg/common/kubernetes/builder.go index b59a6eb..6805eb2 100644 --- a/pkg/common/kubernetes/builder.go +++ b/pkg/common/kubernetes/builder.go @@ -37,10 +37,29 @@ func buildSidecarVpnSecret(namespace, podName, secretValue string) *corev1.Secre } } -func buildSidecarVpnContainer() corev1.Container { +func buildSidecarVpnContainer(privileged bool) corev1.Container { + + // default + imageName := commonModel.SidecarVpnImageName + volumeMounts := []corev1.VolumeMount{ + { + Name: sidecarVpnSecretVolume, + MountPath: secretBasePath, + ReadOnly: true, + }, + } + if privileged { + imageName = commonModel.SidecarVpnPrivilegedImageName + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: sidecarVpnTunnelVolume, + MountPath: sidecarVpnTunnelPath, + ReadOnly: true, + }) + } + return corev1.Container{ Name: fmt.Sprintf("%svpn", commonModel.SidecarPrefixName), - Image: commonModel.SidecarVpnImageName, + Image: imageName, ImagePullPolicy: corev1.PullIfNotPresent, Env: []corev1.EnvVar{ {Name: "OPENVPN_CONFIG", Value: filepath.Join(secretBasePath, sidecarVpnSecretPath)}, @@ -50,31 +69,12 @@ func buildSidecarVpnContainer() corev1.Container { Add: []corev1.Capability{"NET_ADMIN"}, }, }, - VolumeMounts: []corev1.VolumeMount{ - { - Name: sidecarVpnTunnelVolume, - MountPath: sidecarVpnTunnelPath, - ReadOnly: true, - }, - { - Name: sidecarVpnSecretVolume, - MountPath: secretBasePath, - ReadOnly: true, - }, - }, + VolumeMounts: volumeMounts, } } -func buildSidecarVpnVolumes(podName string) []corev1.Volume { - return []corev1.Volume{ - { - Name: sidecarVpnTunnelVolume, - VolumeSource: corev1.VolumeSource{ - HostPath: &corev1.HostPathVolumeSource{ - Path: sidecarVpnTunnelPath, - }, - }, - }, +func buildSidecarVpnVolumes(podName string, privileged bool) []corev1.Volume { + volumes := []corev1.Volume{ { Name: sidecarVpnSecretVolume, VolumeSource: corev1.VolumeSource{ @@ -87,27 +87,40 @@ func buildSidecarVpnVolumes(podName string) []corev1.Volume { }, }, } + if privileged { + volumes = append(volumes, corev1.Volume{ + Name: sidecarVpnTunnelVolume, + VolumeSource: corev1.VolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: sidecarVpnTunnelPath, + }, + }, + }) + } + return volumes } func boolPtr(b bool) *bool { return &b } -func injectSidecarVpn(podSpec *corev1.PodSpec, podName string) { +func injectSidecarVpn(podSpec *corev1.PodSpec, podName string, privileged bool) { // https://kubernetes.io/docs/tasks/configure-pod-container/share-process-namespace //podSpec.ShareProcessNamespace = boolPtr(true) - // disable ipv6, see https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster - podSpec.SecurityContext = &corev1.PodSecurityContext{ - Sysctls: []corev1.Sysctl{ - {Name: "net.ipv6.conf.all.disable_ipv6", Value: "0"}, - }, + if privileged { + // disable ipv6, see https://kubernetes.io/docs/tasks/administer-cluster/sysctl-cluster + podSpec.SecurityContext = &corev1.PodSecurityContext{ + Sysctls: []corev1.Sysctl{ + {Name: "net.ipv6.conf.all.disable_ipv6", Value: "0"}, + }, + } } // inject containers podSpec.Containers = append( // order matters []corev1.Container{ - buildSidecarVpnContainer(), + buildSidecarVpnContainer(privileged), // add fake sleep to allow sidecar-vpn to connect properly before starting the main container { Name: fmt.Sprintf("%ssleep", commonModel.SidecarPrefixName), @@ -127,8 +140,8 @@ func injectSidecarVpn(podSpec *corev1.PodSpec, podName string) { // inject volumes podSpec.Volumes = append( - podSpec.Volumes, // current volumes - buildSidecarVpnVolumes(podName)..., // join slices + podSpec.Volumes, // current volumes + buildSidecarVpnVolumes(podName, privileged)..., // join slices ) } diff --git a/pkg/common/kubernetes/builder_test.go b/pkg/common/kubernetes/builder_test.go index ba5ac66..535aeef 100644 --- a/pkg/common/kubernetes/builder_test.go +++ b/pkg/common/kubernetes/builder_test.go @@ -83,16 +83,10 @@ spec: add: - NET_ADMIN volumeMounts: - - mountPath: /dev/net/tun - name: tun-device-volume - readOnly: true - mountPath: /secrets name: sidecar-vpn-volume readOnly: true volumes: - - hostPath: - path: /dev/net/tun - name: tun-device-volume - name: sidecar-vpn-volume secret: items: @@ -104,8 +98,8 @@ status: {} actual := &corev1.Pod{ Spec: corev1.PodSpec{ - Containers: []corev1.Container{buildSidecarVpnContainer()}, - Volumes: buildSidecarVpnVolumes("main-container"), + Containers: []corev1.Container{buildSidecarVpnContainer(false)}, + Volumes: buildSidecarVpnVolumes("main-container", false), }, } // fix model @@ -135,12 +129,77 @@ spec: add: - NET_ADMIN volumeMounts: - - mountPath: /dev/net/tun - name: tun-device-volume + - mountPath: /secrets + name: sidecar-vpn-volume readOnly: true + - image: busybox + lifecycle: + postStart: + exec: + command: + - sleep + - 3s + name: sidecar-sleep + resources: {} + stdin: true + - args: + - foo + - bar + command: + - xyz + - abc + image: my-image + name: my-name + resources: {} + volumes: + - hostPath: + path: my-path + name: my-volume + - name: sidecar-vpn-volume + secret: + items: + - key: openvpn-config + path: openvpn/client.ovpn + secretName: my-name-sidecar-vpn-secret +status: {} +` + + containerName := "my-name" + actual := newPodSpecTest(containerName) + injectSidecarVpn(&actual.Spec, containerName, false) + // fix model + actual.TypeMeta = metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"} + + assert.YAMLEqf(t, expected, kubernetes.ObjectToYaml(actual), "unexpected pod") +} + +func TestInjectSidecarVpnPrivileged(t *testing.T) { + + expected := ` +apiVersion: v1 +kind: Pod +metadata: + creationTimestamp: null +spec: + containers: + - env: + - name: OPENVPN_CONFIG + value: /secrets/openvpn/client.ovpn + image: hckops/alpine-openvpn-privileged:latest + imagePullPolicy: IfNotPresent + name: sidecar-vpn + resources: {} + securityContext: + capabilities: + add: + - NET_ADMIN + volumeMounts: - mountPath: /secrets name: sidecar-vpn-volume readOnly: true + - mountPath: /dev/net/tun + name: tun-device-volume + readOnly: true - image: busybox lifecycle: postStart: @@ -168,21 +227,21 @@ spec: - hostPath: path: my-path name: my-volume - - hostPath: - path: /dev/net/tun - name: tun-device-volume - name: sidecar-vpn-volume secret: items: - key: openvpn-config path: openvpn/client.ovpn secretName: my-name-sidecar-vpn-secret + - hostPath: + path: /dev/net/tun + name: tun-device-volume status: {} ` containerName := "my-name" actual := newPodSpecTest(containerName) - injectSidecarVpn(&actual.Spec, containerName) + injectSidecarVpn(&actual.Spec, containerName, true) // fix model actual.TypeMeta = metav1.TypeMeta{Kind: "Pod", APIVersion: "v1"} diff --git a/pkg/common/kubernetes/client.go b/pkg/common/kubernetes/client.go index bdb50ba..ee37bbe 100644 --- a/pkg/common/kubernetes/client.go +++ b/pkg/common/kubernetes/client.go @@ -64,7 +64,7 @@ func (common *KubeCommonClient) SidecarVpnInject(namespace string, opts *commonM } // update pod - injectSidecarVpn(podSpec, opts.Name) + injectSidecarVpn(podSpec, opts.Name, opts.NetworkVpn.Privileged) common.eventBus.Publish(newSidecarVpnConnectKubeEvent(opts.NetworkVpn.Name)) return nil diff --git a/pkg/common/model/constant.go b/pkg/common/model/constant.go index becfbde..64db0d9 100644 --- a/pkg/common/model/constant.go +++ b/pkg/common/model/constant.go @@ -5,8 +5,9 @@ const ( KubernetesProvider = "kube" CloudProvider = "cloud" - SidecarPrefixName = "sidecar-" - SidecarVpnImageName = "hckops/alpine-openvpn:latest" - SidecarShareImageName = "busybox" - SidecarShareDir = "/hck/share" + SidecarPrefixName = "sidecar-" + SidecarVpnImageName = "hckops/alpine-openvpn:latest" + SidecarVpnPrivilegedImageName = "hckops/alpine-openvpn-privileged:latest" + SidecarShareImageName = "busybox" + SidecarShareDir = "/hck/share" ) diff --git a/pkg/common/model/types.go b/pkg/common/model/types.go index 7e52318..ced52e9 100644 --- a/pkg/common/model/types.go +++ b/pkg/common/model/types.go @@ -59,6 +59,7 @@ type NetworkVpnInfo struct { Name string LocalPath string ConfigValue string + Privileged bool } type ShareDirInfo struct {