Every app on your laptop or phone talks to servers you never see. A mitmproxy tutorial is the quickest way to watch those conversations, because the free tool sits between a client and the internet and shows each HTTPS request in readable form. Developers use it to debug APIs. Security teams use it to catch cleartext logins, tokens stuffed into URLs, and trackers nobody documented.
The project is moving fast right now. According to the project’s GitHub release page, mitmproxy 12.3.0 is the newest confirmed release, and it reportedly removed eMudhra certificate authorities from the trust list in the precompiled binaries. A Hacker News post about the new –emudhra option captured the change when the flag landed in the main branch on October 21, 2026. For anyone auditing TLS behavior, that makes this a good time to learn the tool.
This mitmproxy tutorial walks through 13 steps in roughly 50 minutes. You will install mitmproxy, trust its certificate authority safely, capture a phone with WireGuard mode, grab traffic from a single process, write a Python addon that flags leaks, and finish with a reusable audit kit that produces a JSON report. If you want more hands-on cybersecurity walkthroughs after this one, the category hub lists them all.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What mitmproxy Does and Why 12.3.0 Matters for Audits
The official site describes mitmproxy as a free and open source interactive HTTPS proxy. Security testers pick it when they need to intercept HTTPS traffic without buying a commercial suite. It can intercept, inspect, modify, and replay HTTP/1, HTTP/2, HTTP/3, and WebSocket traffic, plus other protocols wrapped in SSL/TLS. The project comes from Aldo Cortesi, Maximilian Hils, and Thomas Kriechbaumer, ships under the MIT license, and the site’s GitHub star counter read 46,354 when we fetched it on October 26, 2026.
Three front-ends share one proxy core, and choosing the right one saves time. The documentation says the console tool and the web tool keep every flow in memory, which suits small samples. The command-line tool does not, so it handles long captures and automation better.
| Tool | Interface | Best for | Holds flows in memory |
|---|---|---|---|
mitmproxy | Terminal UI | Keyboard-driven inspection and quick edits | Yes |
mitmweb | Browser UI | DevTools-style review of any app or device | Yes |
mitmdump | Command line | Scripts, CI jobs, long captures (think tcpdump for HTTP) | No |
Why does 12.3.0 matter for audits? The release notes, as summarized in the project’s release commit, describe two things. Precompiled binaries no longer trust the eMudhra certificate authorities, and new options let you sign test certificates with a compromised eMudhra intermediate key so you can check whether your own clients reject that chain. Newsletter and news coverage, including Bitdoze’s October 22 digest and The Diff’s issue No. 019, covers the wider trust-store fallout. Any older blog post claiming the binaries still trust those authorities is out of date after that date.
Recent development also touches the web interface. A change merged around October 18 and 19 fixed the flow-details splitter in mitmweb and added live dragging. Support for displaying NDJSON in content views was added as well. An interactive, editable content view for the web UI exists only as an open pull request as of October 25, so treat it as unreleased. Keep that in mind when you pick a tool for step 2.
Prerequisites and Versions You Need
You do not need much hardware. A laptop, a test phone, and a Wi-Fi network you control cover every step. Here is the checklist, with the versions that matter.
- mitmproxy 12.3.0 or newer. That is the latest release per the project’s GitHub page as of October 26, 2026. Older 12.x builds still run these steps, but they predate the eMudhra trust-list change.
- A current Python 3 release only if you install from PyPI. The standalone binaries bundle their own Python, so the binary route needs none.
- An operating system with local capture support. Local capture works on Windows, Linux, and macOS. On Linux, the docs officially support kernel 6.8 and above, which they note matches Ubuntu 22.04.
- A WireGuard client app on the phone or tablet you want to capture. Any current release from the official stores works.
- Docker (optional) if you prefer the official
mitmproxy/mitmproxyimage. - curl and a text editor. Both are already on most systems.
- A target you own or have written permission to test. The next section explains why this line is not optional.
Run these three commands first so you know what you are working with. The kernel check only matters if you plan to use local capture on Linux.
python3 --version
uname -r
curl --version | head -n 1
Budget your time like this: 10 minutes for install and certificate trust, 15 for phone capture, 10 for filters and replay, and 15 for the addon and the audit kit. The first three steps are the quickest, and the later ones reward patience.
Authorization Rules: Intercept Only What You Own
An HTTPS interception proxy is, by design, a machine-in-the-middle. The same technique that lets you audit your own app can expose another person’s passwords, so the rules matter more than the commands.
- Test your own devices, your own apps, or systems covered by a written scope from the owner.
- Never point other people’s phones or laptops at your proxy, even on a shared home network.
- Treat every capture file as a credential store. Cookies, bearer tokens, and form fields sit in plain text inside saved flows.
- Remove the mitmproxy certificate authority from a device when the audit ends.
- Do not use the technique to defeat certificate pinning in apps you do not control. Pinning exists to stop exactly this.
Employers and clients care about these habits as much as the findings. A short scope note in your audit folder (what you tested, when, and who approved it) takes two minutes and protects you later.
Steps 1 to 3: Install, Pick Your Tool, Run the First Capture
Step 1: Install mitmproxy. You have four good routes. The project site offers a Windows installer, a Microsoft Store listing, Linux binaries, and a Homebrew formula. If you already live in Python, a dedicated virtual environment keeps it away from your system packages. Docker works well when you want a throwaway instance.
# Option A: macOS or Linux with Homebrew
brew install mitmproxy
# Option B: isolated Python install
python3 -m venv ~/.venvs/mitm
. ~/.venvs/mitm/bin/activate
python -m pip install --upgrade pip
python -m pip install mitmproxy
# Option C: Docker (keeps CA files in a host folder)
docker run --rm -it \
-v ~/.mitmproxy:/home/mitmproxy/.mitmproxy \
-p 8080:8080 -p 127.0.0.1:8081:8081 \
mitmproxy/mitmproxy mitmweb --web-host 0.0.0.0
# Confirm the install
mitmdump --version
The version command prints the mitmproxy version, the Python version, and the OpenSSL build. Compare the first line with the release you expect. If it prints something older than 12.3.0, upgrade before continuing.
Step 2: Pick the right tool for the job. Use mitmweb while you learn, since the browser view mirrors Chrome DevTools and makes headers, bodies, and timings easy to scan. Switch to mitmdump for anything unattended. Keep the console mitmproxy for fast keyboard work over SSH. All three accept the same flags, the same modes, and the same addons, so nothing you learn is wasted.
Step 3: Start the proxy and push one request through it. In regular mode you pass no arguments at all. The proxy listens on port 8080 by default. Start mitmweb in one terminal, then send a plain HTTP request from a second terminal.
# Terminal 1
mitmweb
# Terminal 2: cleartext HTTP needs no certificate
curl --proxy http://127.0.0.1:8080 http://example.com/ -o /dev/null -s -w "%{http_code}\n"
Recent releases print a browser URL for the web interface when mitmweb starts, and that URL carries an access token. Open the exact URL from your terminal rather than typing the port by hand. The default web port is 8081. Your curl command should print 200, and the request should appear in the flow list within a second.
Web server listening at http://127.0.0.1:8081/?token=...
Proxy server listening at *:8080
127.0.0.1:41822: client connect
127.0.0.1:41822: GET http://example.com/ HTTP/1.1
<< 200 OK 1.26k
That is example output, and your numbers will differ. The pattern is what counts: a client connect line, the request line, and the response line. If you never see the client connect line, jump to the troubleshooting table later in this guide. Quick sanity check done, so move on to the part that makes HTTPS visible.
Steps 4 and 5: Trust the CA and Intercept HTTPS Traffic
Step 4: Understand the certificate authority. The first time mitmproxy runs, it generates a certificate authority under ~/.mitmproxy. When a client asks for an HTTPS site, mitmproxy creates a certificate for that hostname on the fly and signs it with this authority. The client accepts it only if it trusts the authority. That trust step is the whole trick, and it is also the whole risk.
The folder holds several files. The public certificate comes in .pem, .cer, and .p12 flavors for different platforms. A second file, mitmproxy-ca.pem, contains the private key. Guard it like a password. Anyone who copies it can mint certificates your device would believe.
You have two ways to establish trust. The magic domain mitm.it serves the right certificate file for each device once the device already routes traffic through the proxy. For laptops, the narrower approach is better: trust the authority for a single process instead of the whole machine. Most runtimes read an environment variable for extra roots.
# Per-process trust: nothing changes system-wide
export HTTPS_PROXY=http://127.0.0.1:8080
export HTTP_PROXY=http://127.0.0.1:8080
CA=$HOME/.mitmproxy/mitmproxy-ca-cert.pem
export SSL_CERT_FILE=$CA # many OpenSSL-based tools
export REQUESTS_CA_BUNDLE=$CA # Python requests
export NODE_EXTRA_CA_CERTS=$CA # Node.js
export CURL_CA_BUNDLE=$CA # curl
# System-wide trust on Debian or Ubuntu (remove it after the audit)
sudo cp "$CA" /usr/local/share/ca-certificates/mitmproxy.crt
sudo update-ca-certificates
Step 5: Capture your first HTTPS request. With the environment variables set, ask for an HTTPS page. Curl will ride through the proxy, verify the forged certificate against your bundle, and succeed.
curl -sS https://example.com/ -o /dev/null -w "status=%{http_code} issuer_check=ok\n"
# Prove the interception is real: inspect the certificate curl actually saw
curl -sv https://example.com/ -o /dev/null 2>&1 | grep -i "issuer"
status=200 issuer_check=ok
* issuer: CN=mitmproxy; O=mitmproxy
An issuer line naming mitmproxy confirms that you are looking at the forged certificate, not the real site’s. Open the web interface and click the flow. You will see the decrypted request headers, the response body, the HTTP version, and the TLS details. That moment is the one most people remember from their first mitmproxy tutorial, and it explains why the tool is both loved and carefully handled.
Browsers deserve one extra note. Firefox keeps its own certificate store, so a system-wide install does not reach it. Import the authority inside Firefox settings, or enable its option to trust enterprise roots. Chrome and Edge on Windows and macOS read the system store.
Once you can intercept HTTPS traffic, the skill that separates a useful audit from a pile of screenshots is reading flows with a question in mind. Start with the request line and ask whether the data belongs in that place. A token in a query string leaks into server logs and browser history. A password in a form body over plain HTTP crosses the network readable by anyone on the path. Then read the response headers and look for missing protections such as HSTS and cookie flags. Finally, check where else the app calls, since third-party hosts often carry more user data than the first-party API. Keep those four questions (placement, transport, headers, destinations) next to you, because the audit kit in this guide automates exactly that checklist.
Steps 6 and 7: Capture a Phone With WireGuard Mode
Step 6: Start the WireGuard server. Setting a proxy by hand on a phone works for browsers, but plenty of apps ignore the system proxy. The docs name Android apps as the usual offenders. WireGuard mode fixes that by turning mitmproxy into a small VPN endpoint. The phone joins with a standard WireGuard app, and all its traffic flows through the proxy with no extra routing rules.
The server runs entirely in userspace, so it needs no administrator rights. By default it listens on port 51820 over UDP. Its keys live in ~/.mitmproxy/wireguard.conf, and mitmproxy generates new ones if the file does not exist. Start it, then import the client configuration it prints into the WireGuard app. Scanning the QR code from the console is the fastest route on a phone.
# Start mitmweb with the WireGuard server (default UDP port 51820)
mitmweb --mode wireguard
# Use another port, or run two clients with separate key files
mitmdump --mode wireguard@51821
mitmdump --mode wireguard:wg-keys-1.conf --mode wireguard:wg-keys-2.conf@51821
# Let the phone reach the server through a host firewall (ufw example)
sudo ufw allow 51820/udp
Two limits are worth knowing before you plan the lab. First, you cannot proxy all traffic of the machine that runs mitmproxy this way, because the outgoing WireGuard packets would loop into their own tunnel. Use a second device or switch to local capture for the host. Second, IPv6 support for proxying is limited, which is why the generated client config lists no IPv6 ranges in AllowedIPs. If the phone sits on an unusual network layout, edit the Endpoint address in the config before importing, so it points at the machine’s real LAN address.
You can also narrow the capture. The generated config routes all IPv4 traffic by default (0.0.0.0/0). Change AllowedIPs to a short list of ranges, and only traffic for those destinations enters the proxy. That keeps your banking app and everything else out of the capture, which is both polite and safer.
Step 7: Trust the authority on the phone. With the tunnel up, open http://mitm.it in the phone’s browser and pick the platform. On iOS, install the downloaded profile, then switch on full trust for the mitmproxy root in the certificate trust settings. On Android, install it as a user certificate. Modern Android apps usually ignore user-installed authorities unless the developer opted in, so for your own app, add a debug-only network security configuration. For an emulator, the mitmproxy docs include a how-to on installing a system-level authority.
Then test it. Open any HTTPS page on the phone and watch the flow list. Next, open the app you own. If its requests show decrypted, you are done. If it reports a connection error and the event log shows TLS handshake failures, the app probably pins its certificate. Keep that result as a finding, because it tells you pinning works. Do not try to strip it from software you do not own.
Steps 8 and 9: Local Capture and Reverse Proxy Modes
The docs split proxy modes into a recommended group and an advanced group. You can use any of them with any of the three tools. This table summarizes what each mode does and when to choose it.
| Mode | Example flag | Use it when |
|---|---|---|
| Regular | (no arguments) | The client lets you set an HTTP proxy. Simplest and most dependable. |
| Local capture | --mode local:curl | You want one app or the whole machine without touching proxy settings. |
| WireGuard | --mode wireguard | Phones, tablets, and apps that ignore proxy settings. |
| Reverse | --mode reverse:https://example.com | You sit in front of a server or API and inspect inbound traffic. |
| Transparent | --mode transparent | Custom network routes on Linux or macOS. The docs suggest WireGuard or local capture first. |
| TUN interface | sudo mitmdump --mode tun | Linux only. A virtual network device captures whatever you route to it. |
| Upstream | --mode upstream:http://proxy:8081 | You must chain behind a corporate proxy. |
| SOCKS5 | --mode socks5 | The client speaks SOCKS instead of HTTP proxy. |
| DNS | --mode dns | You want a scriptable DNS server for lab devices. |
Step 8: Capture one process on your own machine. Local capture hooks the operating system at a low level, so the targeted program never knows it is proxied. You can name a process, give a PID, negate a name with an exclamation mark, or list several names with commas. Program names match on the first 16 characters only on Linux.
# Everything on this machine
mitmweb --mode local
# Only curl, only PID 4242, or everything except curl and wget
mitmweb --mode local:curl
mitmweb --mode local:4242
mitmweb --mode local:!curl,!wget
Linux has a few extra rules from the docs. Local capture sees outbound connections only, and it needs root to load its eBPF program. Because mitmproxy asks for the sudo password itself, start mitmweb directly with --mode local on the command line so the prompt appears. Containers are captured only if they use the host network, and Windows Subsystem for Linux is unsupported. On macOS the egress-only limit applies as well.
Step 9: Put mitmproxy in front of your own service. Reverse mode flips the picture. Clients talk to mitmproxy as if it were your server, and mitmproxy forwards each request upstream. It listens on port 8080 unless you append an at-sign and a port to the mode. This is the cleanest way to audit what a front-end sends to your API, or to see what an internal service actually receives.
# Inspect traffic to a dev server on port 8000 through a proxy on 9090
mitmweb --mode reverse:http://127.0.0.1:8000@9090 --web-port 8082
# Point the client at the proxy instead of the server
curl -i http://127.0.0.1:9090/api/health
# Quick TLS front for a toy project (docs example)
mitmdump -p 443 --mode reverse:http://localhost:80/
One caveat from the docs matters in practice. Reverse mode rewrites the Host header to match the upstream server, so absolute links inside returned HTML still point at the original site. A page that navigates away through those links leaves your proxy. For single-page apps that call relative API paths, that rarely hurts. For classic multi-page sites, edit your hosts file so the real name resolves to the proxy.
Steps 10 and 11: Filter, Save, and Replay Flows
Step 10: Learn the filter language. A busy phone produces thousands of flows in minutes, and the filter expressions are how you cut that down. They work in the search box of every tool, in the view_filter option, and as trailing arguments to mitmdump. Combine them with and, or, and not operators, and wrap groups in parentheses.
| Filter | Matches | Audit example |
|---|---|---|
~m POST | Request method | Find every form submission or API write |
~d example.com | Domain | Isolate first-party traffic |
~u /login | URL pattern | Review the authentication flow |
~c 401 | Response status code | Spot broken auth and token expiry |
~t json | Content type | List JSON API calls only |
~bq password | Request body text | Look for credentials sent in the clear |
!~d example.com | Negation | Show only third-party destinations |
Try a couple against your phone capture. The negation filter in the last row is the single most useful audit shortcut, because it answers a question every privacy review starts with: who else is this app talking to? Type it into the web search box, and the list collapses to analytics, ad networks, crash reporters, and CDNs.
Step 11: Save, re-read, and replay. Saving turns a live session into evidence. The commands below come straight from the docs patterns. Writing a file with -w records everything, -nr reads a file without binding the proxy port, and a trailing filter limits what gets written. Client replay with -C sends the recorded requests again, which helps confirm that a fix actually closes a hole.
# Record a session
mitmdump -w session.mitm
# Re-read it later and keep only POST requests
mitmdump -nr session.mitm -w posts.mitm "~m post"
# Replay the recorded requests against the current server (own systems only)
mitmdump -nC posts.mitm -w replayed.mitm
# Add a header to every outgoing request, useful for tagging test traffic
mitmdump --modify-headers '/~q/X-Audit-Run/2026-10-26'
Handle the saved files with care. Cookies and bearer tokens sit in them unmasked. Keep audit captures on an encrypted volume, name them by date and target, and delete them when the engagement closes. The audit kit later in this guide avoids the problem by storing only masked findings in its report.
Step 12: Write Your First Addon
Addons are where mitmproxy stops being a viewer and becomes an audit engine. An addon is a plain Python object whose method names match events such as request, response, load, and done. Mitmproxy picks up whatever sits in a global list called addons. The official site shows how little code it takes to redirect a host or answer a request from the proxy itself.
# counter.py: the smallest useful addon
import logging
class Counter:
def __init__(self):
self.num = 0
def request(self, flow):
self.num += 1
logging.info("We have seen %d flows", self.num)
addons = [Counter()]
Load it with mitmdump -s ./counter.py. The flag is identical for all three tools. Scripts loaded this way are watched for changes, so saving the file re-imports it without restarting the proxy or dropping other addons and in-flight flows. Import-time errors go to the event log, and the previous version stays unregistered until you fix the file and save again. That live reload is why addon work feels fast.
For tests, skip the proxy entirely. Addons are ordinary Python, so import the module, instantiate the class, and call the handler with a flow object built by the test helpers. The docs warn that internal testing helpers carry no stable API promise, so keep such tests small and pin your mitmproxy version in CI.
Step 13: Run Unattended With mitmdump
A manual audit finds issues once. An unattended one catches regressions every build. Mitmdump gives you that, because it needs no terminal UI and streams flows to disk instead of holding them all in memory. Start it with your addon, pass options with --set, drive traffic from a test script or a headless browser, then stop it with a normal termination signal so the done event fires and your report gets written.
mitmdump -q -s audit_addon.py \
--set audit_first_party=example.com \
--set audit_report=out/report.json \
--set stream_large_bodies=1m \
-w out/session.mitm
Three flags carry most of the weight. The quiet flag trims the console to warnings, stream_large_bodies stops giant downloads from filling RAM, and the write flag keeps a raw session for later review. If you drive a browser, use mitmproxy’s Browser addon (the browser.start command in the console tool) or launch Chromium with a proxy flag. The next section ties everything together into one folder you can reuse.
Complete Working Project: A Reusable Privacy Audit Kit
The kit has three files in one folder: the addon that flags problems, a shell script that runs the capture, and a short summarizer that prints a table and sets an exit code for CI. It checks for cleartext HTTP, tokens in URLs, private key blocks, email addresses in outbound requests, missing HSTS headers, and cookies without Secure or HttpOnly. It also counts every destination host, then lists the ones outside your first-party domains.
File 1: audit_addon.py. Note that findings store masked values only. A leaked key never lands in your report in full.
"""audit_addon.py: flag risky traffic, write a JSON report on shutdown."""
import json
import logging
import re
from collections import Counter
from pathlib import Path
from mitmproxy import ctx, http
SECRET_PATTERNS = {
"aws_access_key_id": re.compile(r"AKIA[0-9A-Z]{16}"),
"token_in_url": re.compile(
r"[?&](?:access_token|api_key|apikey|token|auth)=[^&\s]{6,}", re.I
),
"private_key_block": re.compile(
r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----"
),
"email_address": re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"),
}
MAX_BODY = 1_000_000
def mask(text: str) -> str:
return f"{text[:4]}... ({len(text)} chars)"
class Audit:
def __init__(self):
self.findings = {}
self.hosts = Counter()
def load(self, loader):
loader.add_option(
"audit_first_party", str, "", "Comma-separated first-party domains"
)
loader.add_option(
"audit_report", str, "audit-report.json", "Path for the JSON report"
)
def _flag(self, flow: http.HTTPFlow, kind: str, detail: str):
host = flow.request.pretty_host
key = (kind, host, detail)
if key in self.findings:
self.findings[key]["count"] += 1
return
self.findings[key] = {
"kind": kind,
"host": host,
"detail": detail,
"count": 1,
"url": flow.request.pretty_url.split("?")[0],
}
flow.marked = ":warning:"
flow.comment = f"{kind}: {detail}"
logging.warning("AUDIT %s host=%s %s", kind, host, detail)
def request(self, flow: http.HTTPFlow):
req = flow.request
self.hosts[req.pretty_host] += 1
if req.scheme == "http":
self._flag(flow, "cleartext_http", req.path.split("?")[0])
body = ""
if req.raw_content and len(req.raw_content) <= MAX_BODY:
body = req.get_text(strict=False) or ""
headers = [f"{k}: {v}" for k, v in req.headers.items(multi=True)]
haystack = "\n".join([req.pretty_url, *headers, body])
for name, rx in SECRET_PATTERNS.items():
match = rx.search(haystack)
if match:
self._flag(flow, name, mask(match.group(0)))
def response(self, flow: http.HTTPFlow):
resp = flow.response
https = flow.request.scheme == "https"
if https and "strict-transport-security" not in resp.headers:
self._flag(flow, "missing_hsts", flow.request.pretty_host)
for cookie in resp.headers.get_all("set-cookie"):
name = cookie.split("=", 1)[0].strip()
attrs = [a.strip().lower() for a in cookie.split(";")[1:]]
if https and "secure" not in attrs:
self._flag(flow, "cookie_without_secure", name)
if "httponly" not in attrs:
self._flag(flow, "cookie_without_httponly", name)
def done(self):
raw = ctx.options.audit_first_party
first = [s.strip().lower() for s in raw.split(",") if s.strip()]
third = {
host: n
for host, n in self.hosts.items()
if first and not any(host == s or host.endswith("." + s) for s in first)
}
report = {
"hosts": dict(self.hosts.most_common()),
"third_party_hosts": third,
"findings": list(self.findings.values()),
}
path = Path(ctx.options.audit_report)
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(report, indent=2))
logging.info("Audit report written to %s", path)
addons = [Audit()]
File 2: run_audit.sh. The script starts mitmdump in the background, waits for the port, sends four demo requests, and stops the proxy cleanly. It uses the explicit proxy flag for curl, since curl ignores the uppercase HTTP_PROXY variable. Swap the demo URLs for your own app’s staging endpoints. The public echo service in the demo is only a convenience, so replace it if it is unreachable from your network.
#!/usr/bin/env bash
set -euo pipefail
OUT="${1:-out}"
FIRST_PARTY="${2:-example.com}"
CA="$HOME/.mitmproxy/mitmproxy-ca-cert.pem"
PROXY="http://127.0.0.1:8080"
mkdir -p "$OUT"
mitmdump -q -s audit_addon.py \
--set audit_first_party="$FIRST_PARTY" \
--set audit_report="$OUT/report.json" \
--set stream_large_bodies=1m \
-w "$OUT/session.mitm" &
PROXY_PID=$!
# Wait until the proxy port accepts connections
until (exec 3<>/dev/tcp/127.0.0.1/8080) 2>/dev/null; do sleep 0.3; done
[ -f "$CA" ] || { echo "CA not generated yet"; kill "$PROXY_PID"; exit 1; }
c() { curl -sS -o /dev/null --proxy "$PROXY" --cacert "$CA" "$@"; }
c http://example.com/
c "https://httpbin.org/get?api_key=demo123456"
c "https://httpbin.org/cookies/set?session=abc123"
c -d "[email protected]" https://httpbin.org/post
kill "$PROXY_PID"
wait "$PROXY_PID" || true
python3 summarize.py "$OUT/report.json"
File 3: summarize.py. It prints a severity-sorted table, lists third-party hosts, and exits with status 1 when any high-severity finding exists, so a CI job fails the build.
"""summarize.py: print the audit report and fail on high-severity findings."""
import json
import sys
from pathlib import Path
SEVERITY = {
"private_key_block": "high",
"aws_access_key_id": "high",
"token_in_url": "high",
"cleartext_http": "medium",
"cookie_without_secure": "medium",
"email_address": "medium",
"missing_hsts": "low",
"cookie_without_httponly": "low",
}
ORDER = {"high": 0, "medium": 1, "low": 2}
def main(path: str) -> int:
report = json.loads(Path(path).read_text())
rows = sorted(
report["findings"],
key=lambda f: (ORDER[SEVERITY.get(f["kind"], "low")], f["host"]),
)
print(f"{'SEV':<7}{'KIND':<26}{'HOST':<24}DETAIL")
for f in rows:
sev = SEVERITY.get(f["kind"], "low")
print(f"{sev:<7}{f['kind']:<26}{f['host']:<24}{f['detail']} x{f['count']}")
print("\nThird-party hosts:")
for host, n in report["third_party_hosts"].items():
print(f" {host} ({n} requests)")
high = [f for f in rows if SEVERITY.get(f["kind"]) == "high"]
return 1 if high else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else "out/report.json"))
Run it with chmod +x run_audit.sh && ./run_audit.sh out example.com. Your output will look similar to the example below, though hostnames and counts depend on what you send.
SEV KIND HOST DETAIL
high token_in_url httpbin.org ?api... (24 chars) x1
medium cleartext_http example.com / x1
medium email_address httpbin.org jane... (16 chars) x1
medium cookie_without_secure httpbin.org session x1
low missing_hsts httpbin.org httpbin.org x3
low cookie_without_httponly httpbin.org session x1
Third-party hosts:
httpbin.org (3 requests)
Read the result like a reviewer. The high finding means a token traveled in a URL, where proxies, logs, and browser history record it. The medium findings show cleartext traffic and an unmarked cookie. Third-party hosts tell you where data goes. Hook the script into a nightly job, point it at your staging app, and the kit now watches for regressions.
Triage the list in order. High findings go first, because a token in a URL or a key block in a body is usually a real leak, and the fix is to move the secret into an Authorization header or a request body that servers do not log. Medium findings need a decision. Cleartext HTTP to a third-party host is often a forgotten analytics endpoint, and switching to HTTPS is a one-line change. A cookie without the Secure flag matters most when it carries a session. Low findings, such as a missing HSTS header, belong in the backlog unless a policy demands them sooner.
Expect false positives and tune for them. The email pattern fires on any address in a request, including a support contact in a User-Agent string, so add an allowlist once you have seen your own noise. Raise the minimum token length in the URL pattern if your app uses short harmless query keys. Because the addon stores a count and a masked detail for each unique finding, a noisy rule shows up as a large count, which makes tuning quick. After two or three runs, a clean baseline lets you treat any new finding as a regression worth a ticket.
Common Pitfalls That Waste an Afternoon
Most failed sessions trace back to the same handful of mistakes. Check these before you start debugging anything exotic.
- Leaving the certificate authority installed. A trusted mitmproxy root on a daily-use laptop is a standing risk, since anyone who obtains the private key file can forge certificates for that machine. Prefer per-process trust, and remove system-wide installs when you finish.
- Exposing the proxy to the whole network. A proxy bound to every interface acts as an open relay for anyone on the same Wi-Fi. Bind to a specific address with
--listen-host, restrict the port in your firewall, and consider setting proxy authentication. Remember that mitmweb shows captured data too, so keep its port on localhost. - Ignoring QUIC. Browsers may speak HTTP/3 over UDP and skip an HTTP proxy altogether. If a browser tab loads but nothing appears in the flow list, disable QUIC in the browser for the test or use WireGuard or local capture, which see UDP.
- Forgetting the capture files hold secrets. Saved sessions contain cookies, tokens, and personal data. Encrypt them, restrict access, and delete them when the work ends.
- Fighting pinned apps. Pinned apps will refuse the forged certificate on purpose. Test a build you control with pinning relaxed for debugging, or record the pinning as a positive result and move on.
- Running a long capture in the interactive tools. The console and web tools keep every flow in memory. Overnight sessions belong in mitmdump with streaming and a write file.
- Skipping the clock check. A device with a badly wrong date rejects fresh certificates for the wrong reason. Confirm time sync before you blame the proxy.
Troubleshooting: 14 Problems and Fixes
When something breaks, open the event log first. The docs advise looking for client connect messages. If they appear, traffic reaches the proxy and the problem is trust or protocol. If they do not, the network path is wrong. The project’s help forum is the right place for edge cases this table does not cover.
| # | Symptom | Likely cause | Fix |
|---|---|---|---|
| 1 | No flows appear at all | Client never reaches the proxy | Check IP and port, then look for client connect lines in the event log |
| 2 | Wi-Fi device cannot connect to your laptop | Client isolation on the access point | Disable isolation or use a wired lab network |
| 3 | Certificate error in every browser | Authority not trusted | Install it through mitm.it or the per-process variables from step 4 |
| 4 | Firefox still warns after a system install | Firefox uses its own store | Import the certificate in Firefox or enable trust for enterprise roots |
| 5 | Android app ignores the proxy | App bypasses system proxy settings | Switch to WireGuard mode or local capture |
| 6 | Android app rejects the certificate | User authorities not trusted by the app | Use a debug build with a network security config, or an emulator with a system authority |
| 7 | TLS handshake failed for one host | Certificate pinning | Add the host to ignore_hosts for the session, or test a build with pinning off |
| 8 | Browser loads pages but the flow list stays empty | QUIC bypass | Disable QUIC in the browser or capture with WireGuard |
| 9 | WireGuard tunnel never connects | UDP 51820 blocked, or wrong Endpoint | Open the port and correct the Endpoint line before importing the config |
| 10 | Local capture asks for privileges or fails on Linux | Needs sudo, or kernel below 6.8 | Start mitmweb directly with --mode local and check uname -r |
| 11 | Local capture misses a container | Container not on the host network | Run it with --network host |
| 12 | mitmweb page refuses to load | Missing access token in the URL | Open the full URL that mitmweb printed at startup |
| 13 | pip install fails or conflicts | System Python packages clash | Use a virtual environment, pipx, or the standalone binary |
| 14 | Memory climbs during long runs | Interactive tools hold all flows | Switch to mitmdump with stream_large_bodies and a write file |
Docker deserves its own warning. A container does not inherit your host’s trust store, so a proxy that works on the laptop fails inside the container. Mount the authority file into the container and point the runtime variables at it, or build a derived image that installs it. Remove either arrangement before you ship anything.
Advanced Tips for Deeper Audits
- Decrypt in Wireshark. Set the
SSLKEYLOGFILEenvironment variable when you start mitmproxy, then load that file in Wireshark to read the TLS sessions packet by packet. The docs include a how-to on the subject. - Ignore noisy or sensitive hosts. The
ignore_hostsoption passes matching connections through untouched, so your banking domain never gets decrypted. Its counterpartallow_hostsworks the other way round and intercepts only what you list. - Chain behind a corporate proxy. Use upstream mode when your network forces all traffic through another proxy.
- Turn captures into an API spec. The project site lists mitmproxy2swagger, which converts captures into OpenAPI 3.0 specifications. It helps when you inherit an API with no documentation.
- Audit Kubernetes services. The same ecosystem list names kubetap, a kubectl plugin that proxies a service interactively.
- Keep the default client block. The default settings reject clients outside private address ranges, so do not disable that protection on a lab machine with a public IP.
- Test your trust store against the eMudhra change. Release 12.3.0 reportedly adds
--emudhraand--emudhra-oldoptions for signing certificates with a compromised eMudhra intermediate key. Runmitmdump --helpand search the output for emudhra to see the exact behavior in your build, then point a test client at it. A client that accepts the chain still trusts a revoked authority. Docker’s Engine 29 release notes, for comparison, say its Go runtime update rejects chains containing eight revoked eMudhra intermediate certificates.
Beyond that, treat the addon API as your main lever. Everything the tools do is built from the same events, so a ten-line script can tag flows, block a tracker, rewrite a header, or fail a pipeline. Start small, keep reports masked, and let each finding earn its place in the kit.
FAQ: mitmproxy Tutorial Questions Answered
Is it legal to use mitmproxy?
The software is legal and open source. What you do with it is what counts. Intercepting traffic on devices and systems you own, or in a written scope from the owner, is standard practice. Intercepting other people’s traffic without consent can break wiretapping and computer misuse laws in many places.
What is the difference between mitmproxy, mitmweb, and mitmdump?
They are three front-ends on the same proxy core. The first is a terminal UI, the second a browser UI that feels like DevTools, and the third a command-line tool for scripts and long captures. Flags, modes, and addons are shared.
Does mitmproxy support HTTP/3?
The project site lists HTTP/3 among the protocols it can intercept, and the reverse proxy mode accepts http3 and quic schemes. In practice, browsers may bypass an HTTP proxy with QUIC, so confirm that flows actually appear and fall back to WireGuard or local capture if they do not.
How does mitmproxy compare with Burp Suite or OWASP ZAP?
Those tools bundle active scanners, crawlers, and large extension ecosystems for web application testing. Mitmproxy stays focused on traffic inspection and Python scripting, runs headless easily, and works well for mobile and non-browser clients. Many testers use both.
Why does my app show a certificate error through the proxy?
Either the device does not trust the mitmproxy authority, or the app pins its certificate and refuses any other. Check trust first through mitm.it. If the browser works and the app does not, pinning is the likely cause.
Can I capture just one program on my computer?
Yes. Local capture mode takes a process name or PID, such as --mode local:curl, and works on Windows, Linux, and macOS. On Linux it needs root and a supported kernel.
How do I remove the mitmproxy certificate afterwards?
On Debian or Ubuntu, delete /usr/local/share/ca-certificates/mitmproxy.crt and run sudo update-ca-certificates --fresh. On phones, remove the profile or user certificate in settings. Then delete or rotate the files in ~/.mitmproxy if the private key might have leaked.
Is mitmweb ready for daily use?
The documentation still calls it beta, stable for the features it exposes but missing many console features. It is fine for inspection and learning. Use mitmdump or the console tool when you need the full feature set.





