Setting Up mitmproxy With NixOS in Podman
Table of Contents
1. mitmproxy
mitmproxy is an interactive proxy that intercepts TLS communications. There are two main features that makes this program interesting:
- Connection over Wireguard: You can connect the clients via Wireguard, which allows you to MITM any devices you can install Wireguard, and do it over any networks.
- Web frontend:
mitmproxyprovides a subcommandmitmweb, which hosts a website that displays all intercepted traffic over HTTP.
When these two are combined, you can essentially create a self-hosted MITM service that is readily available, with minimal installation process on the devices you want to intercept. I personally use this to do some random checks on some of the applications I use, or whenever I come across some programs that I want to take apart.
2. Nix Config Code
In this article, we will be using rootless Podman. The configuration is written almost exclusively in home-manager except for a single part that opens up port 51820, which will be used for Wireguard communication. Therefore, the config consists of two parts.
The first part is in OS config and goes as follows:
1: { 2: networking.firewall = { 3: enable = true; 4: allowedUDPPorts = [ 51820 ]; 5: }; 6: virtualisation.podman.enable = true; 7: }
What this code does is simple: It enables firewall (just in case I accidentally exclude the one config that sets networking.firewall.enable), and allows 51820 port for Wireguard. It also enables Podman, because the services won't start automatically. Probably an oversight with how systemd triggers Podman service, and this fixes it. The next part, which actually creates the container is within home-manager.
The next part handles the actual creation of the container.
1: { 2: home-manager.users.main.services.podman.containers.mitm = 3: # Define startup script 4: let 5: start = pkgs.writeScriptBin ''podman-start.sh'' '' 6: #!/bin/bash 7: mitmweb --web-host 0.0.0.0 --set confdir=/etc/mitm --set 'web_password=$argon2i$v=19$m=4096,t=3,p=1$c2FsdEl0V2l0aFNhbHQ$jJHYL8FmjFuaY6nOHa5sCJT6qlk2OPWCg/feeJ3rWk0' --set mode=wireguard 8: ''; 9: in 10: { 11: # Set startup script as entrypoint 12: entrypoint = "/etc/podman-start.sh"; 13: image = "mitmproxy/mitmproxy"; 14: labels = { 15: "traefik.enable" = "true"; 16: "traefik.http.routers.mitm-web.entrypoints" = "websecure"; 17: "traefik.http.routers.mitm-web.rule" = "Host(`mitmweb.local`)"; 18: "traefik.http.routers.mitm-web.service" = "s-mitm-web"; 19: "traefik.http.routers.mitm-web.tls" = "true"; 20: "traefik.http.routers.mitm-web.tls.certResolver" = "localca"; 21: "traefik.http.services.s-mitm-web.loadbalancer.server.port" = "8081"; 22: }; 23: network = [ "proxy" ]; 24: ports = [ "51820:51820/udp" ]; 25: volumes = [ 26: "/home/main/.config/sops-nix/secrets/mitm:/etc/mitm" 27: "${start}/bin/podman-start.sh:/etc/podman-start.sh" 28: ]; 29: }; 30: }
This config can be broken down into three parts (and a bit extra):
2.1. Entrypoint
Entrypoint is set through these three steps:
- Create a script using
pkgs.writeScriptBin <Script Name> <Script Content>(Line 5) This creates a file in/nix/store/hashvaluegoeshere/bin/<Script Name>. Unlike files created withpkgs.writeFile, this has executable permission by default. Unlikepkgs.writeShellScriptBin, this does not add NixOS-specific shebang at the front, which would make no sense within the container. - Mount the script via volumes (Line 27)
Note that the script's actual location is
${start}/bin/podman-start.shlike how most binaries end up when they're packages with Nix. - Set entrypoint to that script (Line 12) The script will be run on container startup instead of the usual command.
The entrypoint option available for now does not accept parameters at the time of writing this article, so this particular approach was necessary. This script runs mitmweb, the web frontend, instead of the usual TUI. It also sets the web UI's password with web_password, which accepts an argon2 hash. Oh, and another benefit of using entrypoint script, you don't have to escape as much characters too.
2.2. Volumes
The /etc/mitm directory within the container is set to /home/main/.config/sops-nix/secrets/mitm on the host machine through line 26. This kind of setup is completely optional, and you can just mount /etc/mitm anywhere, and if that directory is writeable, it will create cryptographic materials for you.
2.3. Labels
This part isn't necessary, but I added it just in case you want to see how I set my reverse proxy. I am using Traefik, and that means I need to set certain labels so that Traefik can automatically route traffic to it. There are total of seven labels to set up TLS using my personal certificate authority.
"traefik.enable" = "true";
This line enables Traefik for this container.
"traefik.http.routers.mitm-web.entrypoints" = "websecure";
This is the start of the definition of the router named mitm-web. This makes the router mitm-web to accept traffic from the entrypoint named websecure. The definition of websecure is handled by Traefik, and is not part of the container's config.
"traefik.http.routers.mitm-web.rule" = "Host(`mitmweb.local`)";
This makes the traffic to be handled by mitm-web router if the host is mitmweb.local. In other words, if I visit https://mitmweb.local, I can see the mitmproxy's web UI.
"traefik.http.routers.mitm-web.service" = "s-mitm-web";
If the incoming traffic is to be handled by mitm-web, then the traffic should be redirected to a service named s-mitm-web. The configuration for this service is defined below.
"traefik.http.routers.mitm-web.tls" = "true";
This line enables TLS for the router mitm-web.
"traefik.http.routers.mitm-web.tls.certResolver" = "localca";
This line makes the router to get its TLS certificates from my personal CA, named localca. The definition of this CA is handled by Traefik.
"traefik.http.services.s-mitm-web.loadbalancer.server.port" = "8081";
The service s-mitm-web should access this container's port 8081, which is the port mitmweb uses to serve its website.
The container also need to be in the same network as my Traefik container, which is the proxy network in the config. This is set in line 23.
Note that if you are not interested in reverse proxy, you can remove all the labels, and it should work just fine. Web should also be accessible over port 8081.
3. Output
For me, this creates the following Quadlet file. Note that there are minor differences because I have set extraConfig, which are not necessary, but it modifies dependencies so that it ignores some unnecessary dependencies, and waits for Traefik container to start properly. These can be set with services.podman.containers.mitm.extraPodmanArgs.Unit.
[Container] ContainerName=mitm Entrypoint=/etc/podman-start.sh Environment= Image=mitmproxy/mitmproxy Label=nix.home-manager.managed=true Label=traefik.enable=true Label=traefik.http.routers.mitm-web.entrypoints=websecure Label=traefik.http.routers.mitm-web.rule=Host(`mitmweb.local`) Label=traefik.http.routers.mitm-web.service=s-mitm-web Label=traefik.http.routers.mitm-web.tls=true Label=traefik.http.routers.mitm-web.tls.certResolver=localca Label=traefik.http.services.s-mitm-web.loadbalancer.server.port=8081 Network=proxy PublishPort=51820:51820/udp Volume=/home/main/.config/sops-nix/secrets/mitm:/etc/mitm Volume=/nix/store/mny4frs71sf068hd3zccrav41k9zq08z-podman-start.sh/bin/podman-start.sh:/etc/podman-start.sh [Install] WantedBy=default.target WantedBy=multi-user.target [Quadlet] DefaultDependencies=false [Service] Environment=PATH=/run/wrappers/bin:/run/current-system/sw/bin:/home/main/.nix-profile/bin:/nix/store/ymmaa926pv3f3wlgpw9y1aygdvqi1m7j-systemd-257.6/bin Restart=always TimeoutStopSec=30 [Unit] After=podman-traefik.service Description=Service for container mitm Requires=podman-traefik.service
4. Trusting mitmproxy
To intercept the traffic, you would need to trust the certificate mitmproxy have generated so that the system doesn't complain about how the traffic may be monitored by the attackers. Simply install mitmproxy-ca-cert.pem generated by mitmproxy, and your device will no longer complain about it.
In NixOS, this can be achieved by setting option security.pki.certificateFiles.
1: security.pki.certificateFiles = [ 2: path/inside/nixconfig/repo/mitmproxy-ca-cert.pem 3: ];
5. Enabling Wireguard
If you have not generated the Wireguard configuration yourself, there should be a file named "wireguard.conf" alongside other files in the directory that contains the certificates. This contains both the client and the server's key. Set this connection up on the devices you want to intercept, and you should be good to go.
In NixOS, this can be set using the option networking.wg-quick.interfaces.<Interface Name>.
1: networking = { 2: wg-quick.interfaces = { 3: wg-mitm = { 4: address = [ "10.0.0.1/32" ]; 5: dns = [ "10.0.0.53" ]; 6: privateKeyFile = path/inside/nixconfig/repo/key.txt; 7: peers = [ 8: { 9: publicKey = "<Server's Public Key>"; 10: allowedIPs = [ 11: "0.0.0.0/0" 12: ]; 13: endpoint = "<Server IP>:51820"; 14: } 15: ]; 16: }; 17: }; 18: };
privateKeyFile should be a string that specifies the location of the file that contains the client's private key. It doesn't matter where this is, so you can combine this with sops-nix so that it stays unreadable for the most people.
6. Bonus: Using Wireguard As Proxy To MITM Specific Programs Only
You might want to route only one program through this MITM channel, or run a single command over it. Usually, you can just use mitmproxy as, well, a proxy, and make your program to use your mitmproxy server as a proxy. But you may want to use existing Wireguard configuration as is. For that, you might want to use a tool called wireproxy. This tool allows you to create a proxy that any network traffic that uses that proxy goes over the configured Wireguard tunnel. With this, we can create a readily available proxy, and route traffic through it, only for a single instance of a program, only when we need it.
6.1. Creating A Proxy
We need to create a config file that stores this Wireguard tunnel information. Create a file anywhere you want, but by default wireproxy will read the config from ~/.config/wireproxy.conf.
[Interface] # Address specifies the IP of the Wireguard client (i.e. our device). This seems to be 10.0.0.1 by default. Address = 10.0.0.1/32 # Our private key goes here PrivateKey = <Client's Private Key> # The DNS we will use. This is 10.0.0.53 by default. DNS = 10.0.0.53 # This section specifies the server to connect to. [Peer] # This is the public key of the server. PublicKey = <Server's Public Key> # This is the server we are going to connect to. Endpoint = <Server IP>:51820 # Finally, we need to assign a port that wireproxy will listen to. # Any traffic that uses this as the proxy (if the program supports it, that is) will go through mitmproxy. [http] # This binds local port 45162 to wireproxy. # You can choose any as long as it is valid (above 1024 and under 65535). 45162 below was chosen randomly. # Ensure the port is not in use by any other program. # If you want to know if a port is already in use, enter the following command. If the port is not in use, it should give you nothing. # lsof -i :<Port Number> BindAddress = localhost:45162
Now, we will run wireproxy. Whilst it is running, we can use localhost:45162 as proxy to make the traffic go through mitmproxy.
[nix-shell:~]$ wireproxy DEBUG: 2025/10/17 21:45:20 UAPI: Updating private key DEBUG: 2025/10/17 21:45:20 Routine: encryption worker 12 - started ...
6.2. MITMing cURL ATTACH
To test this out, we will use curl, and see if any results come up in the webpage. -x localhost:45162 is what you need to include to use the proxy, and ultimately, MITM the traffic. -k was included to ignore the SSL warnings you will get if you have not added mitmproxy's certificate to the list of trusted certificates.
[main@main:~]$ curl -k -x localhost:45162 https://ip.me <My server's IP appeared here because ip.me gives you your own IP>
When you run this, you should see some entries appearing in mitmweb.