Transport security is the first line of defense for your app's data in transit. Android gives you several layers: platform TLS defaults, Network Security Config, and certificate pinning.
Android Network Security Config
The network_security_config.xml file declaratively controls your app's network security policy — no code required for most use cases.
<!-- res/xml/network_security_config.xml -->
<network-security-config>
<!-- Block cleartext traffic everywhere -->
<base-config cleartextTrafficPermitted="false">
<trust-anchors>
<certificates src="system" /> <!-- trust system CAs -->
</trust-anchors>
</base-config>
<!-- Allow cleartext for a specific debug host -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">10.0.2.2</domain> <!-- emulator localhost -->
</domain-config>
</network-security-config>
<!-- AndroidManifest.xml -->
<application
android:networkSecurityConfig="@xml/network_security_config" ...>
Certificate Pinning
Pinning ensures your app only trusts a specific certificate (or public key), preventing MitM attacks even if a CA is compromised.
Option 1: Network Security Config (declarative)
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2026-01-01">
<!-- Primary pin: SHA-256 of the SubjectPublicKeyInfo -->
<pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
<!-- Backup pin: different cert (rotate before primary expires) -->
<pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
</pin-set>
</domain-config>
</network-security-config>
Get pin values:
openssl s_client -connect api.example.com:443 | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | base64
Option 2: OkHttp CertificatePinner (programmatic)
val certificatePinner = CertificatePinner.Builder()
.add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup
.build()
val okHttpClient = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
If the pin doesn't match, OkHttp throws SSLPeerUnverifiedException — treat this as a security event and alert your backend.
Risks of Certificate Pinning
Pinning requires careful management:
| Risk | Mitigation |
|---|---|
| Certificate expires without pin update | Always include a backup pin; set expiration reminders |
| App update lag (users on old versions) | Use public key pinning (more stable than cert pinning) |
| Pin mismatch blocks all users | Have a server-side kill switch / config to disable pinning |
| Testing with Charles/Fiddler breaks | Maintain a debug build without pinning; never ship without it |
Detecting MitM Proxies (Testing)
# Trust a custom CA for debugging — safe in debug builds only
adb shell settings put global http_proxy 192.168.1.100:8888
In your debug Network Security Config, add the proxy CA:
<debug-overrides>
<trust-anchors>
<certificates src="system" />
<certificates src="user" /> <!-- allows user-installed CAs like Charles -->
</trust-anchors>
</debug-overrides>
Never include <certificates src="user"/> in release builds — it allows interception.
TLS Version & Cipher Suites
Android 10+ enforces TLS 1.2 minimum by default. Android 12+ makes TLS 1.3 preferred. You can restrict further with a custom SSLSocketFactory in OkHttp, but the Android defaults are already strong.
// Verify TLS version in use
val connection = URL("https://api.example.com").openConnection() as HttpsURLConnection
println("Protocol: ${connection.cipher}")
Key Takeaways
| Concept | Rule |
|---|---|
| Network Security Config | Use to block cleartext and configure trust anchors declaratively |
| Certificate pinning | Always include ≥ 2 pins (primary + backup); use public key pinning |
| Pin rotation | Update app before certificate expires; never pin to leaf cert alone |
| Debug builds | Allow user CAs in debug only; never in release |
SSLPeerUnverifiedException | Log as security event, not a normal error |