Android does not trust the proxy certificate

Apps built for Android 7+ ignore user-installed CAs. How to allow them in a debug build with network_security_config, and what it will not fix.

What it means

The certificate is installed, Android lists it in settings, Chrome on the phone works, and the app still dies with SSLHandshakeException: Trust anchor for certification path not found. A trust anchor is the root certificate that path validation starts from (RFC 5280 § 6.1.1), and the app accepts none here: an app built for Android 7.0 (API 24) or newer trusts only system certificate authorities and ignores everything the user added. That is deliberate: it stops anyone holding your phone from reading an app's traffic.

How to fix it

The app has to allow user certificates itself, and only in a debug build.

Create app/src/main/res/xml/network_security_config.xml:

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <debug-overrides>
        <trust-anchors>
            <certificates src="user" />
            <certificates src="system" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

Point the manifest at it:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... >

<debug-overrides> is the whole trick. The block applies only when the app is built with android:debuggable="true". Gradle sets that flag on debug builds and never on release ones, so this file is safe to ship.

Do not use a plain <base-config> with src="user" instead. It works, and it works in release too, which is the exact hole Android closed.

One host rather than all of them:

<domain-config>
    <domain includeSubdomains="true">api.example.com</domain>
    <trust-anchors>
        <certificates src="user" />
    </trust-anchors>
</domain-config>

This one cannot go inside <debug-overrides>, which accepts nothing but <trust-anchors>. So a <domain-config> is live in release builds as well. Keep it out of them by putting a separate copy of the file in app/src/debug/res/xml/: Gradle then uses it for the debug variant only.

Two more things that trip people up:

  • The cutoff is the app's targetSdkVersion, not the phone's Android version.
  • The config is compiled into the APK. After editing it, rebuild and reinstall the app, because restarting it changes nothing.

What this will not fix

Certificate pinning. The app may check the fingerprint itself: OkHttp's CertificatePinner, a <pin-set> in the same config, a hand-written TrustManager. Then none of the above helps, because the app rejects a certificate the system already trusts. Pinning comes out in code (drop CertificatePinner in the debug variant), or it does not come out at all.

Flutter. Dart has its own TLS stack. It ignores network_security_config and never looks at user-installed certificates. You need a SecurityContext carrying your CA, or badCertificateCallback in a debug build.

Somebody else's app. No source, nowhere to put the config. Two options left: install the certificate as a system CA (needs root, plus a Magisk or KernelSU module such as cert-fixer or AlwaysTrustUserCerts), or repatch the APK with apk-mitm.

Android 14+. The system CA store moved out of /system/etc/security/cacerts into the read-only com.android.conscrypt APEX, at /apex/com.android.conscrypt/cacerts. The old «drop a file into /system» tricks fail there, so pick a module that knows the APEX path. User certificates did not move.

See it on your own traffic

Once the app trusts the certificate, its requests show up in full: host, path, headers, request and response bodies. Before that Solpuga shows only a connection that opened and died at once, which is the sign that network_security_config has not taken effect yet. Setting the proxy on the phone is a separate step. Full setup steps: Mobile Devices.

View in Solpuga

Related