← back to blog

how to use mitmproxy with cloud phones for SSL inspection

May 06, 2026

how to use mitmproxy with cloud phones for SSL inspection

if you need scriptable HTTPS inspection from a cloud Android phone, mitmproxy is the right tool. mitmproxy is open source, runs from the command line, supports interactive (mitmproxy), web UI (mitmweb), and headless (mitmdump) modes, and lets you write Python addons that rewrite traffic on the fly. unlike Charles Proxy, it costs nothing and integrates cleanly into CI for automated traffic capture.

cloud phones from cloudf.one connect to mitmproxy the same way physical Android devices do: set the device’s WiFi proxy to point at the mitmproxy listener, install the mitmproxy root CA on the device, decrypt HTTPS for the hosts you choose. this guide walks the full setup including the mid-level Python addon you’ll likely need for any serious traffic-modification workflow.

why mitmproxy beats Charles for automated work

three reasons teams pick mitmproxy over Charles. it is scriptable in Python via the addons API, so you can rewrite responses, inject headers, simulate failures, and capture flows in code. it runs headless, which fits CI and Docker. it is open source, which means no licensing for fleets and no per-seat fees.

Charles is faster for one-off interactive debugging. mitmproxy is faster for repeatable, scripted, automated work. our Charles Proxy setup post covers the interactive path.

prerequisites

on your laptop or CI runner: Python 3.9 or newer, mitmproxy installed, network reachability between the cloud phone and your laptop.

# install mitmproxy on macOS
brew install mitmproxy

# or via pip
pip install mitmproxy

# verify
mitmproxy --version

for context on cloud phone basics, see cloud phone vs physical Android device and how to set up ADB on cloudf.one.

step 1: start mitmproxy

run mitmweb (the web UI version) for first-time setup:

mitmweb --listen-host 0.0.0.0 --listen-port 8080

mitmweb starts the proxy on port 8080 and opens a browser at http://localhost:8081 for the inspection UI. listening on 0.0.0.0 is critical because the cloud phone needs to reach the proxy from outside localhost.

step 2: bridge the network gap

cloud phones run in Singapore. your laptop is elsewhere. you need to expose port 8080 to the cloud phone.

option A: ngrok TCP tunnel.

ngrok tcp 8080

ngrok prints tcp://2.tcp.ngrok.io:14837. that becomes the proxy host:port for the cloud phone.

option B: cloudf.one reverse tunnel.

cloudfone tunnel reverse --local 8080 --device $DEVICE_ID

both work the same way. the cloud phone connects to the public hostname, traffic flows back to your laptop’s mitmproxy.

step 3: configure the cloud phone’s proxy

via ADB:

adb -s adb-sg.cloudf.one:5555 shell settings put global http_proxy proxy.host.example:14837

# bounce WiFi
adb -s adb-sg.cloudf.one:5555 shell svc wifi disable
adb -s adb-sg.cloudf.one:5555 shell svc wifi enable

verify:

adb -s adb-sg.cloudf.one:5555 shell settings get global http_proxy

step 4: install the mitmproxy root CA

on the cloud phone via cloudf.one screen, open the browser and visit http://mitm.it. mitmproxy serves a download page with a CA cert for each platform.

tap the Android cert. download it. the file is mitmproxy-ca-cert.pem.

install via Settings, Security and privacy, More security and privacy, Encryption and credentials, Install a certificate, CA certificate. browse to Downloads, select the cert.

alternatively via ADB:

adb -s adb-sg.cloudf.one:5555 push ~/.mitmproxy/mitmproxy-ca-cert.pem /sdcard/Download/

then install through Settings.

step 5: confirm HTTPS decryption works

on the cloud phone, open Chrome and visit https://example.com. in mitmweb, you should see the request flow. click on it; you’ll see decrypted headers and body.

if you see “SSLHandshake: certificate unknown,” the CA is not trusted. re-check step 4.

if no requests appear at all, the proxy setting is not active. re-check step 3.

step 6: write a Python addon for traffic rewriting

the real power of mitmproxy is in addons. create addon.py:

from mitmproxy import http

class TokenInjector:
    def request(self, flow: http.HTTPFlow) -> None:
        if "api.example.com" in flow.request.pretty_host:
            flow.request.headers["X-Test-Token"] = "injected-by-test"

    def response(self, flow: http.HTTPFlow) -> None:
        if flow.request.path == "/api/v1/feature-flags":
            flow.response.text = '{"feature_x": true, "feature_y": false}'

addons = [TokenInjector()]

run mitmproxy with the addon:

mitmdump -s addon.py --listen-port 8080

now every request to api.example.com gets the test token, and every response from /api/v1/feature-flags gets a fixed payload. great for testing app behavior under specific server states without touching the real backend.

step 7: capture flows for CI assertions

for CI, you want to capture flows and assert on them.

# capture.py
from mitmproxy import http
import json

captured = []

class FlowCapture:
    def response(self, flow: http.HTTPFlow) -> None:
        captured.append({
            "url": flow.request.pretty_url,
            "method": flow.request.method,
            "status": flow.response.status_code,
            "request_body": flow.request.text,
            "response_body": flow.response.text,
        })

    def done(self):
        with open("flows.json", "w") as f:
            json.dump(captured, f, indent=2)

addons = [FlowCapture()]

run as part of CI:

mitmdump -s capture.py --listen-port 8080 &
MITM_PID=$!
sleep 2

# run your test against the cloud phone
maestro --device adb-sg.cloudf.one:5555 test flows/checkout.yaml

# stop mitmproxy
kill $MITM_PID
sleep 1

# assert on captured flows
python -c "
import json
flows = json.load(open('flows.json'))
checkout = [f for f in flows if '/api/v1/checkout' in f['url']]
assert len(checkout) == 1, f'expected 1 checkout call, got {len(checkout)}'
assert checkout[0]['status'] == 200
print('checkout flow verified')
"

this is automated network-level assertion, which catches bugs that UI tests miss. our CI/CD integration guide covers wrapping this in GitHub Actions.

step 8: handle certificate-pinned apps

apps that pin certificates refuse the mitmproxy CA the same way they refuse Charles. the workarounds are identical: opt in via Network Security Config (if you own the app), patch with Frida at runtime (for security research), or build a debug variant with pinning disabled.

mitmproxy’s --set ssl_insecure=true does not bypass pinning; it bypasses the client’s verification of the server, which is the opposite of what pinning checks.

tear down

when done:

# remove proxy from cloud phone
adb -s adb-sg.cloudf.one:5555 shell settings put global http_proxy :0
adb -s adb-sg.cloudf.one:5555 shell svc wifi disable
adb -s adb-sg.cloudf.one:5555 shell svc wifi enable

remove the CA via Settings, Security, User credentials.

stop mitmproxy and ngrok.

try mitmproxy on a real Singapore cloud phone

register for a free trial for a one-hour cloud phone token. install mitmproxy, set up the proxy, install the CA, write a 5-line addon. once confirmed, scale to a paid plan for automated traffic capture in CI.

frequently asked questions

what’s the difference between mitmproxy, mitmweb, and mitmdump?

mitmproxy is the interactive curses-based UI. mitmweb is a web UI on localhost:8081. mitmdump is headless, logs flows to stdout, runs in CI. all three speak the same protocol and load the same addons.

does mitmproxy work with HTTP/2 and HTTP/3?

yes for HTTP/2 (since mitmproxy 4.x). HTTP/3 (QUIC) support is partial as of 2026 and depends on the mitmproxy version. for full HTTP/3 inspection, check the mitmproxy docs for the current state.

can I run mitmproxy in Docker for CI?

yes. the official mitmproxy/mitmproxy Docker image runs mitmdump headless. mount your addon and capture file as volumes.

what about apps using certificate pinning?

same as Charles. opt-in via Network Security Config, runtime patch with Frida, or use a debug build. mitmproxy itself cannot bypass pinning.

is mitmproxy slower than Charles?

no. both run on the same TCP loop. mitmproxy is sometimes faster because it does not maintain a GUI. for high-throughput captures (gRPC streams, WebSocket bursts), mitmproxy handles the load better than Charles.