Open, self-hosted Secure Access Service Edge components built from OSS tooling, declared end-to-end with Nix.
OpenSASE is a TLS-inspection edge. Clients connect over OpenVPN; an mitmproxy addon decrypts and re-encrypts traffic per a URL-category policy; every payload is scanned by ClamAV; every verdict is written to a JSONL decision log. Run it from a Raspberry Pi to a rack server, for a team or for a household.
Two halves, one codebase:
- The SASE edge — policy-driven inspection for teams and servers.
- The home appliance — install it like a Pi-hole and every family device routes through it: per-device policy and blocking, full traffic visibility, and malware verdicts on what you choose to decrypt. Planned additions — agent (MCP) inspection, DNS-layer policy, short-lived certs, chat (XMPP) with attachment scanning — are tracked in Future and on the roadmap board.
The stack is modular by design: the bare-minimum decrypt edge (dnsmasq + openvpn + mitmproxy + policy) stands alone; scanning and future modules layer on as opt-ins (compose profiles are tracked on the roadmap; today the full compose file below is the one deployment).
Two deployment paths, one source of truth:
- Containers — pull the Nix-built OCI images from GHCR and
docker compose upon Linux, macOS, or Windows. - Nix / NixOS appliance — the same stack as stock NixOS modules: build a QEMU VM,
nixos-rebuilda physical or remote host.
flowchart LR
client["VPN client<br/>(any)"]
subgraph edge["OpenSASE edge"]
vpn["openvpn<br/>udp/5443"]
mitm["mitmproxy<br/>tcp/8080 explicit · TPROXY 80/443 transparent<br/>splice or bump per URL category"]
clam["clamav (clamd)<br/>tcp/3310 INSTREAM"]
dns["dnsmasq<br/>udp+tcp/53"]
log[("decision log<br/>decisions.jsonl")]
end
net((Internet))
client -- "OpenVPN (TLS 1.2, tls-crypt)" --> vpn
vpn --> dns
vpn -- "HTTP/HTTPS" --> mitm
mitm -- "clamd INSTREAM scan" --> clam
mitm -- "Every verdict" --> log
mitm -- "clean traffic" --> net
mitm -. "INFECTED: blocked + logged" .-> client
Verdict order: passlist (splice, no decrypt) → bumplist (decrypt + scan) → default bump. Every verdict — splice, bump, clean, INFECTED — is written to /data/log/decisions.jsonl. The addon speaks clamd's INSTREAM protocol directly.
All images are built by Nix — no Dockerfile builds. CI (.github/workflows/release-images.yml) builds each image with dockerTools.buildLayeredImage from the flake and pushes to GHCR on v* tags, with an SPDX SBOM per image:
ghcr.io/shsingh/opensase-dnsmasq— DNS for VPN clientsghcr.io/shsingh/opensase-clamav— clamd scanner (DB bootstraps on first run,SKIP_FRESHCLAM=1to skip)ghcr.io/shsingh/opensase-mitmproxy— decrypt/re-encrypt core; addon + policy lists baked into the imageghcr.io/shsingh/opensase-openvpn— VPN server (config + certs supplied by you)
The same images build locally: nix run .#load-images.
Releases are GPG-signed tags. The release workflow verifies the tag (git tag -v) and refuses to publish unsigned material:
VERSION=v0.1.1
git tag -s $VERSION -m "OpenSASE $VERSION: release summary"
git push origin $VERSIONCI matrix-builds the four images on Linux runners, publishes to GHCR (:latest + version), and opens a draft release with generated notes: image-digest table, commit changelog since the previous tag, SPDX SBOMs as assets, and a compose deploy snippet. Review and publish the draft.
Any OCI runtime: Docker Engine / Docker Desktop on Linux, macOS, Windows, or Podman.
git clone https://github.com/shsingh/opensase && cd opensase
docker compose -p opensase up -dThe OpenVPN server requires a PKI before it starts:
nix run .#vpn-init # one-time CA bootstrap into ./state/openvpnCopy ./state/openvpn/* into the openvpn_priv volume and restart the openvpn service. Point a client at udp/5443 and an explicit proxy at <host>:8080.
Install Nix on any Linux distribution, or run NixOS:
git clone https://github.com/shsingh/opensase && cd opensase
# 1. Bootstrap the OpenVPN CA + certs (gitignored ./state)
nix run .#vpn-init
# 2a. QEMU VM (Linux host):
nix build .#vm-x86_64 && ./result/bin/run-opensase-vm # aarch64: .#vm-aarch64
# 2b. Real machine or remote host:
nixos-rebuild switch --flake .#opensase
nixos-rebuild switch --flake .#opensase --target-host root@<ip>
# 2c. Container stack from locally built images:
nix run .#load-images && docker compose -p opensase up -dTo add the edge to an existing NixOS host, import nix/appliance.nix — it composes stock modules (services.clamav, services.dnsmasq, services.openvpn) plus the services.opensase module.
Declarative base manifests are committed: k8s/manifests.cue is the single
service model (rendered to k8s/manifests.yaml by
nix run .#k8s-manifests, or cue export ./k8s -e list --out yaml — apply
with kubectl apply -f k8s/manifests.yaml). The service contract:
| Container | Image | Capabilities | Persistent volume |
|---|---|---|---|
| dnsmasq | opensase-dnsmasq |
— | RWO for /var/lib/misc |
| clamav | opensase-clamav |
— | RWO for /var/lib/clamav (signature DB) |
| mitmproxy | opensase-mitmproxy |
— | RWO for /data (decision log) |
| openvpn | opensase-openvpn |
NET_ADMIN |
RWO for /data-priv (server.conf + PKI) |
Deployment requirements:
- PKI distribution:
server.conf,ca.crt,server.crt/key,ta.keymust exist under/data-privbeforeopenvpnstarts. Inject via Secret + podpostStartcopy, or mount from aSecurityContext-protected volume. Current images check no paths; the k8s follow-up adds a strict readiness gate. - LoadBalancer / NodePort for
udp/5443(VPN) andtcp/53(DNS); mitmproxy8080is ClusterIP for explicit-proxy clients orLoadBalancerfor TPROXY testing. - Transparent mode on k8s: unsupported. TPROXY policy routing inside a container network was unreliable in the original design and remains docker/compose-only as explicit proxy. On Kubernetes, run mitmproxy explicit (
8080) behind a Service; transparent interception needs CNI-level support (e.g. a Layer 7 plugin or patched sidecar). - clamd scale: one replica; the signature DB bootstraps on first start — attach a PVC to avoid re-download on restart.
services.opensase.enable = true;
services.opensase.mitmMode = "regular"; # or "transparent" (TPROXY 80/443)
services.opensase.listenPort = 8080;
services.opensase.policyPass = ./my/pass.txt;
services.opensase.policyBump = ./my/bump.txt;
services.opensase.decisionLog = "/var/lib/opensase/log/decisions.jsonl";nix/policy/pass.txt— domains spliced through, never decryptednix/policy/bump.txt— domains always decrypted and scanned
Both are types.path NixOS options, overridable at rebuild time. In the container images they are baked in per tag; to change policy, rebuild the image or mount your own lists into the mitmproxy container.
- Copy the generated
.ovpnprofile into OpenVPN's config directory; connect as Administrator (the tunnel requires it). - Trust the mitmproxy CA: double-click the
.crt, install for the current user, into Trusted Root Certification Authorities. Chrome and Edge read this store; Firefox has its own (Settings → Certificates).
- Import the profile into OpenVPN Connect or Tunnelblick; trust the mitmproxy CA in the System keychain.
- OpenVPN Connect → import the
.ovpn; CA: open the.crtfrom Files and trust it in the profile.
With the tunnel up:
ping <appliance> # tunnel reachable
curl -x http://<appliance>:8080 https://example.com # explicit-proxy pathDownload the EICAR test file over HTTPS: the download is blocked and the decision log records INFECTED.
- One process per container; the appliance runs services under dedicated non-root users.
- The VPN CA lives in its own volume — protect it.
- TLS 1.2, elliptic-curve certificates, DHE, tls-crypt.
- Not for production as-is — this is a lab/testing appliance. TLS interception is a high-value target; review bump/splice policy before extending.
Start at TROUBLESHOOTING.md — a stages ladder (tunnel → DNS → HTTP flow → verdicts → scanning) with tcpdump/tshark/jq commands at each stage, what the decision log should show, and the cert-trust gotchas (device clocks, CA installation, the #3 HMAC family).
Full documentation site (architecture, deployment, policy, CI, roadmap, troubleshooting, future directions): https://shsingh.github.io/opensase/ — Quarto, built from docs/.
See CONTRIBUTING.md for branching, conventional signed commits, and the acceptance suite; SECURITY.md for reporting a vulnerability (never as a public issue); CODE_OF_CONDUCT.md.
- NixOS flake: appliance + QEMU VMs,
nix flake check --all-systemsclean - Nix-built OCI images for all four services + GHCR release workflow
- Compose deployment for non-Nix users (Linux/macOS/Windows)
- Quarto docs site → GitHub Pages
- OpenSSF Scorecard workflow + badge; OpenSSF Best Practices project 15121
- Kubernetes base manifests, declarative:
k8s/manifests.cue(CUE) →nix run .#k8s-manifests - Kubernetes: hardening overlay (PKI Secret + readiness gate) on the CUE base
- First release:
v0.1.1— images on GHCR (:latest+:0.1.1), release published, SBOMs attached - VM closure build + boot smoke test (CI, linux runner)
- Live verdict verification (EICAR over HTTPS)
Inspired by @sweitzel's docker-vpnbox.
GPL-3.0 (inherited from the original project).