Shadowsocks proxy in Kubernetes

Published: 2026-06-02

Tor is slow. SOCKS5 has no encryption. Shadowsocks sits between the two: it's a lightweight encrypted proxy protocol designed to be indistinguishable from random HTTPS noise. The setup on this server runs shadowsocks-libev inside a Kubernetes pod and exposes it as a NodePort.

Why Shadowsocks

Shadowsocks was originally built to bypass DPI (deep packet inspection) in China. The protocol:

  • wraps all traffic in AES-256-GCM (or ChaCha20) encrypted frames
  • looks like random TCP data — no recognizable handshake
  • supports both TCP and UDP
  • has clients for every platform (Android, iOS, Windows, macOS, Linux)

For a personal VPS, this means traffic between your device and the server is encrypted and not fingerprinted as "this is a proxy."

Kubernetes setup

Everything lives in a dedicated shadowsocks namespace. The password is stored in a Secret and injected as an env variable:

yamlapiVersion: v1
kind: Secret
metadata:
  name: ss-auth
  namespace: shadowsocks
stringData:
  password: "your-password-here"

The Deployment uses shadowsocks/shadowsocks-libev and passes flags as args. TCP and UDP share the same port, which requires both containerPort entries:

yamlapiVersion: apps/v1
kind: Deployment
metadata:
  name: shadowsocks
  namespace: shadowsocks
spec:
  replicas: 1
  selector:
    matchLabels:
      app: shadowsocks
  template:
    metadata:
      labels:
        app: shadowsocks
    spec:
      containers:
      - name: ss-server
        image: shadowsocks/shadowsocks-libev:latest
        args:
        - ss-server
        - -s
        - "0.0.0.0"
        - -p
        - "8388"
        - -m
        - aes-256-gcm
        - -k
        - $(SS_PASSWORD)
        - -u           # enable UDP relay
        - --no-delay
        - --reuse-port
        - -v
        ports:
        - containerPort: 8388
          hostPort: 8388
          protocol: TCP
          name: ss-tcp
        - containerPort: 8388
          hostPort: 8388
          protocol: UDP
          name: ss-udp
        env:
        - name: SS_PASSWORD
          valueFrom:
            secretKeyRef:
              name: ss-auth
              key: password
        resources:
          requests:
            cpu: "10m"
            memory: "32Mi"
          limits:
            cpu: "200m"
            memory: "64Mi"

The Service exposes both protocols on the same port:

yamlapiVersion: v1
kind: Service
metadata:
  name: shadowsocks
  namespace: shadowsocks
spec:
  type: NodePort
  selector:
    app: shadowsocks
  ports:
  - name: ss-tcp
    port: 8388
    targetPort: 8388
    nodePort: 30388
    protocol: TCP
  - name: ss-udp
    port: 8388
    targetPort: 8388
    nodePort: 30388
    protocol: UDP

The -u flag and UDP relay

The -u flag enables UDP relay. Without it, Shadowsocks only proxies TCP. DNS queries in SOCKS5 mode typically go over UDP, so without -u you'd need to handle DNS separately. With -u, the client can send UDP packets through the tunnel and DNS just works.

Choosing the cipher

aes-256-gcm is the default and works well on most hardware. On low-end VMs without AES-NI acceleration, chacha20-ietf-poly1305 is faster:

bash# check AES-NI support
grep -m1 aes /proc/cpuinfo

If the flag is present, stick with aes-256-gcm. If not, switch to chacha20-ietf-poly1305. The performance difference on a 1-core VPS can be 3–5x for bulk transfers.

Client setup

After deployment, connect with any Shadowsocks client:

Host:     <your-vps-ip>
Port:     30388
Password: <value from secret>
Method:   aes-256-gcm

On macOS/iOS Shadowrocket works well. On Android v2rayNG supports Shadowsocks natively. On Linux ss-local from the shadowsocks-libev package exposes a local SOCKS5 port:

bashss-local -s <vps-ip> -p 30388 -m aes-256-gcm -k <password> -l 1080
# now set SOCKS5 proxy to 127.0.0.1:1080
curl --socks5 127.0.0.1:1080 https://ifconfig.me

Setup script

k0s-setup/20-setup-shadowsocks.sh generates a random password if none is set, creates the namespace, Secret, and Deployment, then prints connection details:

bashSS_PORT=30388 SS_METHOD=aes-256-gcm ssh root@server 'bash -s' < k0s-setup/20-setup-shadowsocks.sh

The script auto-detects the public IP and prints a ready-to-use config at the end.

Firewall

NodePort services open the port in kube-proxy's iptables rules, but the host firewall also needs to allow it. On a k0s VM with ufw:

bashufw allow 30388/tcp
ufw allow 30388/udp

Without the UDP rule, ss-local -u (UDP relay) will silently fail and DNS lookups will time out.

Resource usage

At idle, shadowsocks-libev uses ~2MB RAM and essentially no CPU. Under active use (1 connected client), CPU stays under 10m on a shared vCPU. The limits set in the Deployment (200m / 64Mi) are comfortable headroom.


Summary

Setting Value
Image shadowsocks/shadowsocks-libev:latest
Cipher aes-256-gcm
Port (NodePort) 30388
UDP relay enabled (-u)
Password storage Kubernetes Secret
Idle RAM ~2 MB