Chisel
Tunnel server for TCP and reverse SOCKS tunnels over a websocket
Install
Deploy Chisel with ZopDay. It can provision the cluster in your cloud account for you.
Or add the zop.dev chart repository and install chisel yourself.
helm repo add zopdev https://helm.zop.dev
helm repo update
helm install tunnel zopdev/chisel --version v0.0.3 Chisel is a tunnel server for TCP and reverse SOCKS tunnels over a websocket, secured with SSH. Clients dial the server over a websocket and open tunnels through it. With reverse remotes, a client opens a listener on the server, for example a SOCKS proxy into the client’s own network.
This chart runs one chisel server. Its users come from a Secret, or from a sidecar that polls a gRPC method and keeps the users file in sync.
Prerequisites
- Kubernetes 1.26+
- Helm 3+
- An ingress controller, only with
ingress.enabled=true. The default annotations suit ingress-nginx. - A CNI that enforces NetworkPolicy, only with
networkPolicy.enabled=true - The Prometheus Operator CRDs, only with
alerts.enabled=true
Install
helm repo add zopdev https://helm.zop.devhelm repo updatehelm install tunnel zopdev/chiselThe chart generates a server key and one user. Read the user and the fingerprint:
kubectl get secret tunnel-chisel-auth -o jsonpath='{.data.users\.json}' | base64 -dkubectl logs deploy/tunnel-chisel -c chisel | grep FingerprintConnect a client:
chisel client --fingerprint <fingerprint> --auth chisel:<password> \ http://tunnel-chisel:8080 R:0.0.0.0:11001:socksGive --fingerprint in full, all 44 characters with the trailing =.
chisel 1.12 clients reject a shortened fingerprint.
Configuration
| Key | Default | Description |
|---|---|---|
image.repository | zopdev/chisel | Built from images/chisel, amd64 only. On arm64 nodes, use jpillora/chisel with a chisel version tag. |
image.tag | v0.0.1 | v0.0.1 holds chisel 1.12.1. With jpillora/chisel, the tag is the chisel version and must be 1.11.7 or later for per-user ACLs. |
keepalive | 25s | Websocket keepalive. 0s turns it off. |
key.existingSecret | "" | Secret with server.key. Empty generates one. Fixed after install. |
auth.existingSecret | "" | Secret with users.json. Empty (and feed off) generates one user. |
feed.enabled | false | Keep users.json in sync from a gRPC method. |
feed.address | "" | host:port of the gRPC server. Called without TLS. |
feed.method | "" | Full method name, e.g. pkg.Service/GetAuthFile. |
feed.proto | "" | The .proto that defines the method, as text. |
feed.responseField | "" | Response field with the users file, e.g. authFileJson. |
feed.interval | 5 | Seconds between polls. |
reversePorts.start, reversePorts.end | 0, 0 | Reverse ports, on their own ClusterIP Service <release>-chisel-reverse. At most 101, from 1024 up. 0 means none. |
service.type | ClusterIP | Type of the websocket Service: ClusterIP, NodePort or LoadBalancer. The reverse ports Service is always ClusterIP. |
ingress.enabled | false | Create an Ingress. Needs ingress.host. |
ingress.className | "" | IngressClass. |
ingress.host | "" | Hostname. |
ingress.path, ingress.pathType | /, Prefix | Path clients dial. |
ingress.annotations | ingress-nginx websocket settings | No buffering, 3600 s timeouts. |
ingress.tlsSecretName | "" | TLS Secret for the host. |
networkPolicy.enabled | false | Create a NetworkPolicy. |
networkPolicy.reversePortsFrom | [] | Peers that may reach the reverse ports. |
alerts.enabled | false | PrometheusRule on kube-state-metrics. |
resources | requests 50m / 64Mi, limits 500m / 512Mi | chisel container resources. |
nodeSelector, tolerations, affinity | empty | Pod scheduling. |
The websocket port is always 8080, on the pod and on the Service <release>-chisel.
Server key
Clients pin the server’s fingerprint, so the key must not change.
The generated key is read back on every upgrade and kept on helm uninstall.
Tools that render with helm template, such as Argo CD, cannot read it back and would make a new key on every sync.
With them, make the key once and set key.existingSecret:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD:/out" zopdev/chisel:v0.0.1 server --keygen /out/server.keykubectl create secret generic chisel-key --from-file=server.keyUsers
chisel lets everyone in when the users file is empty. The chart never starts chisel without at least one user.
- Generated (default): one user
chiselthat may open only reverse listeners on all interfaces (^R:0\.0\.0\.0:[0-9]+$), with a random password, kept across upgrades and on uninstall. It cannot open forward remotes, so it cannot reach what the pod can reach. - Static:
auth.existingSecretnames a Secret with ausers.jsonentry, in chisel’s format:{"user:pass": ["<remote-regex>", ...]}. It must hold at least one user. chisel reloads it when the kubelet updates the mounted Secret, so no restart is needed. - Feed:
feed.enabled=true. See below.
Feed
The feed sidecar calls feed.method on feed.address every feed.interval seconds with an empty request, without TLS.
It reads feed.responseField from the response, a string that holds the whole users.json, and writes it when it changed.
chisel reloads the file on its own.
- Before the first good answer, chisel has only a placeholder user that can open nothing.
- An empty or invalid answer, or a failed call, keeps the last good file. Open tunnels keep working.
- The file is written in place in an in-memory volume, because Secret and ConfigMap volumes are read-only. chisel 1.12 watches the file’s directory and reloads about 100 ms after a write.
A subset of the .proto with the service, the method and its two messages is enough:
feed: enabled: true address: users.apps.svc.cluster.local:9000 method: users.UserService/GetTunnelUsers responseField: authFileJson proto: | syntax = "proto3"; package users; service UserService { rpc GetTunnelUsers(GetTunnelUsersRequest) returns (GetTunnelUsersResponse); } message GetTunnelUsersRequest {} message GetTunnelUsersResponse { string auth_file_json = 1; }Reverse ports
A client’s reverse listener lives on the server pod.
To reach it from other pods, put its port in reversePorts.start..reversePorts.end.
The chart puts these ports on a separate Service, <release>-chisel-reverse, which is always ClusterIP, so service.type never exposes them.
The client must bind it on all interfaces, R:0.0.0.0:<port>:socks; a bare R:<port>:socks binds to 127.0.0.1 inside the pod.
Point the SOCKS consumers at the reverse Service, not the websocket one.
For a release named tunnel, a SOCKS consumer in another namespace uses socks5://tunnel-chisel-reverse.<namespace>.svc.cluster.local:<port>.
A reverse port has no login of its own: whoever reaches it uses the tunnel.
Turn on networkPolicy.enabled and list the allowed peers in networkPolicy.reversePortsFrom.
Ingress
Clients do not follow redirects.
For a path other than /, set ingress.pathType=Exact: ingress-nginx answers /tunnel with a redirect for a Prefix of /tunnel/.
chisel ignores the path, so no rewrite is needed.
Clients dial https://<host><path> exactly as written, with no trailing slash.
NetworkPolicy
- Ingress: the websocket port from anywhere (it has its own login), the reverse ports from
reversePortsFromonly. - Egress: with the feed on, DNS (ports 53 and 5353) and the port of
feed.address. With the feed off, none. - NetworkPolicy matches the destination pod’s port, after Service translation. So
feed.addressmust name a Service whoseportequals itstargetPort, for example a gRPC Service that maps 9000 to 9000. - chisel needs no egress for reverse tunnels. Forward remotes, where the server dials out, do not work with the policy on.
Uninstall
helm uninstall tunnelThe generated Secrets are kept, so a reinstall with the same release name keeps the fingerprint and the password. Delete them by hand to remove everything:
kubectl delete secret tunnel-chisel-key tunnel-chisel-authNotes
zopdev/chiselis built for amd64 only. On arm64 nodes, the pod fails withexec format error. There, setimage.repository: jpillora/chiselandimage.tagto a chisel version (1.11.7or later), for example1.12.1.- One replica, by design: reverse ports live on the pod the client is connected to. Updates use the
Recreatestrategy, so two pods never share the Service. - The pod runs as uid 100 (the grpcurl image’s user) with a read-only root filesystem and no service account token.
/healthbacks both probes.- The server must be chisel 1.11.7 or later. Older servers do not enforce the per-user ACLs in
users.json.
License
This project is licensed under the LICENSE. Chisel itself is MIT licensed.