Octelium documentation · Latest

Self-Hosted VPN and Exit Nodes via SOCKS5

Octelium can very easily operate as your own self-hosted exit node and a lightweight self-hosted alternative to consumer VPNs via the SOCKS5 Service mode (read more about the SOCKS5 mode here). Your traffic leaves to the internet from a network that you control, whether it is the network of your Cluster, your home network behind NAT, a cloud VM in another region, or even the Tor network. Unlike a commercial VPN provider, there is no third party that sees your traffic, and unlike running a standalone SOCKS5 proxy exposed to the internet, every single connection is authenticated, authorized and audited by the Cluster. Octelium provides the following:

  • Exit via the Cluster's own network by deploying a SOCKS5 proxy as a managed container (read more about managed containers here) with secretless access, so that your Users never see or manage the proxy password.

  • Exit via any connected octelium client from anywhere behind NAT (e.g. a home server, a Raspberry Pi, a laptop, a VM in a different cloud region, etc...) through the embedded SOCKS5 mode and the built-in socks5.octelium Service.

  • Exit via the Tor network, including access to .onion services, by deploying a Tor client as a managed container.

  • Identity-based, context-aware access control on a per-connection basis according to the requested destination host, port and address type via policy-as-code with CEL and OPA (read more about Policies and access control here).

  • Dynamic routing to different exit nodes according to identity as well as the requested destination (read more about dynamic configuration here).

  • OpenTelemetry-native, identity-based, application-layer aware visibility where the destination of every connection as well as the transferred bytes are logged in real time (read more here).

  • Per-application exit, where only the applications you explicitly configure (e.g. a browser profile, curl, etc...) use the exit node instead of all the traffic of your host.

  • GitOps-friendly declarative, programmable management (read more here).

note

The SOCKS5 mode currently supports only the SOCKS5 CONNECT command. In other words, only TCP-based traffic goes through the exit node, while UDP-based traffic such as QUIC/HTTP3 and WebRTC is not proxied. Web browsers simply use TCP-based HTTP/1.1 and HTTP/2 instead whenever they are configured to use a SOCKS5 proxy.

Exit via the Cluster

The simplest exit node is the Cluster itself. In this example, we deploy go-socks5-proxy, a small open source SOCKS5 server that's published as the serjs/go-socks5-proxy image and is built on a distroless image running as a non-root user, as a managed container. The traffic of your Users then leaves to the internet from the public IP address of the Cluster's nodes. For example, if your Cluster is installed on a cloud VPS in another country, then you effectively have your own personal VPN server located in that country.

The serjs/go-socks5-proxy container requires a username and a password by default. Instead of sharing that password with your Users, we store it in a Secret (read more about Secrets here) that's used both by the container itself as well as by the Service to authenticate to the container on behalf of your Users. First, we create the Secret with a randomly generated password as follows:

octeliumctl create secret --value "$(openssl rand -hex 32)" exit-password

Now we create the Service as follows:

kind: Service metadata: name: exit spec: mode: SOCKS5 port: 1080 config: upstream: container: image: serjs/go-socks5-proxy:latest port: 1080 env: - name: PROXY_USER value: octelium - name: PROXY_PASSWORD fromSecret: exit-password resourceLimit: cpu: millicores: 500 memory: megabytes: 128 socks5: auth: usernamePassword: username: octelium password: fromSecret: exit-password

The PROXY_PASSWORD environment variable is set from the exit-password Secret (read more here), and the socks5.auth field instructs the Service to authenticate to the upstream SOCKS5 server via the same Secret (read more about secretless access for SOCKS5 here). Therefore, the password never leaves the Cluster.

note

Your Users never reach the managed container directly since all of their connections go through the Service. However, the managed container runs inside the Kubernetes cluster that runs the Cluster. Keeping the container password-protected prevents other workloads running inside the same Kubernetes cluster from using it as an open proxy.

You can now apply the creation of the Service as follows (read more here):

octeliumctl apply /PATH/TO/SERVICE.YAML

Once connected to the Cluster via octelium connect (read more here), you can test the exit node, for example, via curl as follows:

curl --socks5-hostname exit:1080 https://checkip.amazonaws.com

The returned IP address should now be the public IP address of your Cluster rather than your own.

note

The --socks5-hostname flag, as well as the socks5h:// scheme, instructs the SOCKS5 client to send the destination domain name to the proxy instead of resolving it locally. This way the domain name is resolved at the exit node side which prevents DNS leaks from your own network. This also enables the Service to see the requested domain name itself, which is required for domain-based access control and visibility. On the other hand, the --socks5 flag resolves the domain name locally and only sends the resolved IP address to the proxy.

You can also connect via the rootless mode and map the Service to your host's localhost (read more here) without having to run octelium connect as root as follows:

# Connect as non-root/unprivileged OS user octelium connect -p exit:1080 # Now use the exit node at localhost curl --socks5-hostname localhost:1080 https://checkip.amazonaws.com

Many command-line tools, including curl, also respect the ALL_PROXY environment variable. This enables you to route all the requests of a shell session through the exit node as follows:

export ALL_PROXY=socks5h://localhost:1080 curl https://checkip.amazonaws.com
note

The serjs/go-socks5-proxy container is stateless. You can, therefore, increase the number of its replicas in order to scale it horizontally (read more here).

Web Browsers

You can now use the exit node from your web browser while the rest of your host's traffic is unaffected. For example, in Firefox, go to Settings, then Network Settings, choose Manual proxy configuration, set the SOCKS Host to localhost and the Port to 1080, choose SOCKS v5 and enable Proxy DNS when using SOCKS v5 in order to resolve domain names at the exit node side.

For Chromium-based browsers (e.g. Google Chrome, Brave, Microsoft Edge, etc...), you can launch a dedicated browser profile that always uses the exit node as follows:

chromium --user-data-dir="$HOME/.config/chromium-exit" \ --proxy-server="socks5://localhost:1080" \ --host-resolver-rules="MAP * ~NOTFOUND , EXCLUDE localhost"

The --host-resolver-rules flag prevents the browser from resolving domain names locally, while the --user-data-dir flag keeps your ordinary browser profile untouched.

Exit via Connected Clients

Exiting via the Cluster is not the only way. In many cases, you might want your traffic to exit from your own home network while you are traveling, or from a VM in a certain cloud region, or from an office machine. Octelium enables any connected octelium client to act as an exit node via the embedded SOCKS5 mode (read more here). In this mode, the SOCKS5 server runs inside the octelium client itself and the connections to the final destinations originate from the host running the client, even if that host is behind NAT without any open ports.

The socks5.octelium Service

The Cluster comes with a built-in socks5.octelium Service in the octelium Namespace that operates in the embedded SOCKS5 mode. It is automatically created upon the Cluster installation and its configuration looks as follows:

kind: Service metadata: name: socks5.octelium displayName: SOCKS5 Server for Embedded Access spec: mode: SOCKS5 config: socks5: isEmbeddedMode: true

Since the Service does not explicitly set a port, it listens over the default SOCKS5 port 1080. Now, in order to turn some host, for example your home server, into an exit node, you only need to connect to the Cluster from that host via the --esocks5 flag (read more here) as follows:

export OCTELIUM_DOMAIN=<DOMAIN> octelium connect --esocks5

You can also enable the embedded SOCKS5 server via the OCTELIUM_ESOCKS5 environment variable instead of the --esocks5 flag as follows:

export OCTELIUM_ESOCKS5=true octelium connect
note

By default, the embedded SOCKS5 server listens only over the private tunnel addresses of the connected Session. In other words, the embedded SOCKS5 server is not reachable from your local network, and is only reachable through the Cluster. The domain names requested by your Users are resolved by the exit node host itself.

A headless exit node is typically a WORKLOAD User (read more about User types here). You can create the User as follows:

kind: User metadata: name: home-server spec: type: WORKLOAD

And then create an authentication token Credential for it (read more here) as follows:

octeliumctl create cred --user home-server home-server-token

Now you can run the exit node anywhere. For example, you can run it as a Docker container (read more here) on your home server or Raspberry Pi as follows:

docker run -d --name octelium-exit --restart unless-stopped \ ghcr.io/octelium/octelium connect --domain <DOMAIN> --auth-token <AUTHENTICATION_TOKEN> --esocks5
note

The above container does not need any additional Linux capabilities. Without the NET_ADMIN capability, the octelium client uses the unprivileged gVisor netstack mode (read more here), and the embedded SOCKS5 server listens from within that netstack while it reaches the final destinations through the container's own network.

You can also deploy an exit node inside any remote Kubernetes cluster, for example a managed Kubernetes cluster in a certain cloud region, via the official Helm chart (read more here) as follows:

helm install exit-eu oci://ghcr.io/octelium/helm-charts/octelium --set octelium.domain=<DOMAIN> --set octelium.auth.token=<AUTHENTICATION_TOKEN> --set octelium.esocks5.enabled=true

To use a certain exit node, you need to set the name of its Session as the SOCKS5 username. You can list the Cluster's Sessions as follows:

octeliumctl get sess

The output of the command above should look as follows:

NAME | USER | TYPE | EXPIRES IN | AGE | STATE | CONNECTED ---------------------+-------------+--------+------------+-----+--------+------------ home-server-x8k2qa | home-server | CLIENT | 83d | 2h | ACTIVE | True john-9o2ztp | john | CLIENT | 83d | 6d | ACTIVE | True

Now, from your own machine, you can exit via the home-server-x8k2qa Session as follows:

curl --socks5-hostname home-server-x8k2qa@socks5.octelium:1080 https://checkip.amazonaws.com

The returned IP address should now be the public IP address of your home network. You can also map the socks5.octelium Service to your localhost via the rootless mode as follows:

octelium connect -p socks5.octelium:1080 curl --socks5-hostname home-server-x8k2qa@localhost:1080 https://checkip.amazonaws.com
note

A Session name is not permanent. For example, a container that authenticates via the --auth-token flag upon every start creates a new Session, and therefore a new Session name, every time it restarts. Moreover, most web browsers do not natively support SOCKS5 username/password authentication, which the embedded mode requires in order to select the exit node. If you need a stable exit node address or want to use the exit node from your web browser, then you might want to serve a SOCKS5 server from behind NAT as shown below.

Restricting Embedded Exit Nodes

It's important to understand that any User who is authorized to access an embedded SOCKS5 Service can use any connected Session that serves embedded SOCKS5 as an exit node, as long as the User knows the Session's name. Octelium has no notion of a "superuser", so by default no User can access the socks5.octelium Service unless explicitly allowed by a Policy. However, if some of your Users are granted broad access, for example via the allow-all Policy (read more here), then they can use your exit nodes too.

Since socks5.octelium is a built-in Service that is managed by the Cluster itself, you might want to create your own embedded SOCKS5 Service with its own inline Policies instead. Here is an example where only the members of the family Group can use the embedded exit nodes:

kind: Service metadata: name: esocks5 spec: mode: SOCKS5 port: 1080 config: socks5: isEmbeddedMode: true authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: match: '"family" in ctx.user.spec.groups'

Now members of the family Group can use the exit node as follows:

curl --socks5-hostname home-server-x8k2qa@esocks5:1080 https://checkip.amazonaws.com

You can additionally deny access to the built-in socks5.octelium Service for everybody else via a DENY rule as follows:

kind: Policy metadata: name: restrict-socks5-octelium spec: rules: - effect: DENY condition: all: of: - match: ctx.service.metadata.name == "socks5.octelium" - not: '"family" in ctx.user.spec.groups'

You can then enforce such a Policy across the entire Cluster as a global Policy (read more here).

note

Note that we used the not condition (read more here) instead of negating the expression itself inside a match condition (i.e. !("family" in ctx.user.spec.groups)). A User that does not belong to any Group at all has no groups field. In such case, an expression that refers to that missing field never matches, while the not condition correctly matches.

Serving a SOCKS5 Proxy from Behind NAT

Instead of the embedded mode, you can also run any SOCKS5 server behind NAT and remotely serve it via a connected octelium client as an ordinary SOCKS5 Service (read more about remotely serving upstreams here). This way your Users use a stable Service name, without having to know any Session name, and they can use it from their web browsers too.

For example, you can run the serjs/go-socks5-proxy container on your home server and bind it only to its localhost as follows:

docker run -d --name socks5 --restart unless-stopped -p 127.0.0.1:1080:1080 \ -e PROXY_USER=octelium -e PROXY_PASSWORD=<PASSWORD> serjs/go-socks5-proxy:latest

Now we store the same password in a Secret as follows:

octeliumctl create secret --value <PASSWORD> home-password

And then create the Service whose upstream is served by the home-server User as follows:

kind: Service metadata: name: home spec: mode: SOCKS5 port: 1080 config: upstream: url: tcp://localhost:1080 user: home-server socks5: auth: usernamePassword: username: octelium password: fromSecret: home-password

Now the home server needs to connect to the Cluster and serve the Service via the --serve flag (read more here) as follows:

export OCTELIUM_DOMAIN=<DOMAIN> octelium connect --auth-token <AUTHENTICATION_TOKEN> --serve home

Now your Users can use the exit node via the stable address home:1080, for example, as follows:

curl --socks5-hostname home:1080 https://checkip.amazonaws.com
note

You can have several connected octelium clients serving the same Service. In such case, the Service load balances the connections among all of them (read more here).

Exit via Tor

You can also deploy a Tor client as a managed container and use it as an exit node to the Tor network. In this example, we use tor-socks-proxy, an actively maintained, minimal open source Tor client image that's based on Alpine Linux. The container runs Tor as the unprivileged tor user and listens with its SOCKS5 port at 9150. Here is an example:

kind: Service metadata: name: tor spec: mode: SOCKS5 port: 9050 config: upstream: container: image: peterdavehello/tor-socks-proxy:latest port: 9150 resourceLimit: cpu: millicores: 1000 memory: megabytes: 512 socks5: auth: noAuth: true

Tor's SOCKS5 port does not require any authentication. Therefore, we explicitly instruct the Service to use no authentication when connecting to the upstream via the noAuth field. We also set the Service port to 9050, the conventional port of the Tor SOCKS5 proxy, while the container itself listens over the port 9150.

note

The peterdavehello/tor-socks-proxy image is currently published for the amd64 architecture only. If your Cluster's data-plane nodes are ARM-based (e.g. a Raspberry Pi-based homelab Cluster), you need to build the image yourself from its open source Dockerfile and deploy it from your own container registry (read more about private registries here).

You can now apply the creation of the Service via octeliumctl apply as shown above. Note that Tor might need a few moments to bootstrap its circuits once the container starts. You can now test the exit node via the Tor Project's own check API as follows:

curl --socks5-hostname tor:9050 https://check.torproject.org/api/ip

The output should look as follows:

{"IsTor":true,"IP":"203.0.113.42"}

You can also reach .onion services. For example, you can reach the DuckDuckGo onion service as follows:

curl --socks5-hostname tor:9050 -I https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/
note

.onion addresses can only be resolved by the Tor network itself. Therefore, you must use a SOCKS5 client that sends the domain name to the proxy (e.g. the --socks5-hostname flag or the socks5h:// scheme for curl, or enabling Proxy DNS when using SOCKS v5 in Firefox) rather than resolving it locally.

You can also force Tor to use exit relays from certain countries. Tor accepts any configuration option as a command-line argument that overrides the container's torrc configuration file, so you only need to override the container's command and arguments (read more here) as follows:

kind: Service metadata: name: tor-eu spec: mode: SOCKS5 port: 9050 config: upstream: container: image: peterdavehello/tor-socks-proxy:latest port: 9150 command: - /usr/bin/tor args: - -f - /etc/tor/torrc - --ExitNodes - "{de},{nl},{ch}" - --StrictNodes - "1" socks5: auth: noAuth: true
note

Restricting the exit relays to a few countries reduces the number of available relays and can weaken the anonymity properties of Tor. Moreover, it's important to understand that, unlike the Tor Browser, using Tor from an ordinary browser does not protect you against browser fingerprinting. Also note that the Cluster itself still authenticates your Users and logs the destinations of their connections. In other words, Tor here hides your Cluster's IP address from the destinations rather than hiding the identity of your Users from the Cluster.

Dynamic Configuration

You can also combine several exit nodes in a single Service and dynamically route each connection to a certain exit node according to the identity of the User as well as the requested destination (read more about dynamic configuration here). Here is an example of an all-in-one exit Service, which can replace the exit Service shown above, that automatically routes .onion destinations, as well as all the connections of the members of the tor-only Group, to Tor while all other connections exit via the Cluster:

kind: Service metadata: name: exit spec: mode: SOCKS5 port: 1080 dynamicConfig: configs: - name: direct upstream: container: image: serjs/go-socks5-proxy:latest port: 1080 env: - name: PROXY_USER value: octelium - name: PROXY_PASSWORD fromSecret: exit-password socks5: auth: usernamePassword: username: octelium password: fromSecret: exit-password - name: tor upstream: container: image: peterdavehello/tor-socks-proxy:latest port: 9150 socks5: auth: noAuth: true rules: - condition: any: of: - match: ctx.request.socks5.connect.host.endsWith(".onion") - match: '"tor-only" in ctx.user.spec.groups' configName: tor - condition: matchAny: true configName: direct

Now any User can simply use the address exit:1080 and reach both the ordinary internet as well as .onion services without having to switch between proxies.

Access Control

An exit node is effectively an open door from your network to the internet. Octelium enables you to control access to it on a per-connection basis according to the SOCKS5 request information, which is available in ctx.request.socks5 (read more here), in addition to the identity and context of the User (read more about Policies and access control here). Here is a detailed example for the exit Service where only the members of the family Group are allowed to use the exit node, while connections to SMTP ports, which are commonly abused to send spam, as well as to private, loopback and link-local IP addresses (e.g. the 169.254.169.254 metadata endpoint of cloud providers) are denied:

kind: Service metadata: name: exit spec: mode: SOCKS5 port: 1080 config: # The rest of the config as shown above authorization: inlinePolicies: - spec: rules: - effect: DENY condition: any: of: - match: ctx.request.socks5.connect.port in [25, 465, 587] - match: ctx.request.socks5.connect.addressType != "DOMAIN" && net.isPrivateIP(ctx.request.socks5.connect.host) - match: ctx.request.socks5.connect.addressType != "DOMAIN" && net.isIPInRange(ctx.request.socks5.connect.host, "127.0.0.0/8") - match: ctx.request.socks5.connect.addressType != "DOMAIN" && net.isIPInRange(ctx.request.socks5.connect.host, "169.254.0.0/16") - effect: ALLOW condition: match: '"family" in ctx.user.spec.groups'

The net.isPrivateIP and net.isIPInRange functions (read more about Octelium's CEL functions here) only work with IP addresses, which is why each expression first checks that the requested destination is not a domain name. Note that IP-based rules only apply when the requested destination is an IP address, since a domain name is resolved by the upstream SOCKS5 server itself. If you want to be even stricter, you can only allow domain-based destinations via the expression ctx.request.socks5.connect.addressType == "DOMAIN".

Authentication

HUMAN Users can seamlessly authenticate to the Cluster via their web browsers using IdentityProviders (read more here). There are currently 3 types:

  • GitHub OAuth IdentityProvider as shown in detail here

  • OpenID Connect IdentityProviders (e.g. Okta, Auth0, etc...) as shown here.

  • SAML 2.0 IdentityProviders (e.g. Okta, Entra ID, etc...) as shown here.

Furthermore, you can register a FIDO2 Authenticator (e.g. Yubikeys) in order to directly login later via Passkey (read more here) without having to use an IdentityProvider.

Visibility

Octelium also provides OpenTelemetry-ready, application-layer L7 aware visibility and access logging in real time (read more about visibility here). Each SOCKS5 connection emits a CONNECT access Log once it is authorized as well as a SESSION_END access Log once it ends (read more here). Here is an example of a SESSION_END access Log of a connection to a .onion service that was routed to the Tor exit node:

{ "apiVersion": "core/v1", "entry": { "common": { // Omitted for the sake of brevity of the example }, "info": { "socks5": { "type": "SESSION_END", "host": "duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion", "port": 443, "addressType": "DOMAIN", "receivedBytes": "1874", "sentBytes": "70312", "upstreamHost": "upstream-svc-exit-tor", "upstreamPort": 9150 } } }, "kind": "AccessLog", "metadata": { // Omitted for the sake of brevity of the example } }

Here are a few more features that you might be interested in:

  • Exposing your homelab's DNS server, such as Pi-hole, to all of your connected Users (read more here).

  • Embedded SSH to remotely access the hosts of your exit nodes (read more here).

  • Persistent volumes, resource limits and probes for your managed containers (read more here).

  • Serving your exit nodes from inside remote Kubernetes clusters via Helm (read more here).

  • Application layer-aware ABAC access control via policy-as-code using CEL and Open Policy Agent (read more here).