9.8 — App Transport Security
Opening scenario
Your app makes a request to http://legacy-analytics.acme-internal.com and it silently fails on iOS 17 with no error in the console — just an empty response. Or it works in development on the simulator but fails on devices. App Transport Security (ATS) is doing its job: blocking plaintext traffic. The cure isn’t disabling ATS globally; it’s understanding the exception system and using it surgically.
Context — what ATS enforces
Since iOS 9, ATS has been on by default for any networking through NSURLSession, NSURLConnection, or CFNetwork. The defaults require:
| Requirement | Value |
|---|---|
| Scheme | https:// only |
| TLS version | ≥ 1.2 |
| Cipher suite | Forward-secret (ECDHE, DHE) |
| Certificate | RSA ≥ 2048-bit or ECC ≥ 256-bit |
| Hash | SHA-256 or stronger |
Plain HTTP is blocked. TLS 1.0/1.1 is blocked. Self-signed certs are blocked. Most legacy on-prem APIs you might integrate with violate at least one rule.
The Info.plist key
ATS is configured via the NSAppTransportSecurity dictionary:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy-analytics.acme-internal.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<false/>
</dict>
</dict>
</dict>
Domain-scoped exceptions are surgical and explicit. The build tooling can audit them; reviewers can see them in diffs.
The escape hatches (and when they’re acceptable)
| Key | Effect | Acceptable use |
|---|---|---|
NSAllowsArbitraryLoads | Disables ATS globally | Almost never. Red flag in audits. |
NSAllowsArbitraryLoadsInWebContent | Disables ATS only for WKWebView | Apps that legitimately render arbitrary user-supplied web content (browsers, RSS readers) |
NSAllowsArbitraryLoadsForMedia | Disables ATS for AVFoundation media loads | Apps streaming legacy HTTP video sources |
NSAllowsLocalNetworking | Allows plaintext to .local, IP literals, unqualified hostnames | Apps controlling IoT devices, smart speakers, printers |
The pattern: domain exceptions are fine when justified and documented. Global NSAllowsArbitraryLoads requires App Store Review justification and often gets the app rejected without a written reason in the review notes.
Per-domain exception keys
<key>api.legacy-vendor.com</key>
<dict>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.1</string>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSRequiresCertificateTransparency</key>
<false/>
</dict>
Each key relaxes a specific requirement. Be minimal — if the vendor supports TLS 1.2 with FS, only set those keys, don’t blanket allow HTTP.
App Store Review and ATS
Apple’s review notes when an app contains NSAllowsArbitraryLoads:
“Your app contains the NSAllowsArbitraryLoads key in your Info.plist. Please remove this key or provide reasonable justification for its use.”
Acceptable justifications:
- The app is a general-purpose web browser
- The app integrates with a specific legacy enterprise system documented in a screenshot
- The app processes user-supplied URLs (e.g., RSS reader, link previewer)
Unacceptable:
- “We need to call a third-party tracking API that doesn’t support HTTPS”
- “Development convenience”
- “We didn’t have time to fix our backend”
Apps with unjustified ATS exceptions get rejected, and the rejection cycle adds days to releases.
Local development workaround
In dev, a common need is hitting http://localhost:8080 for a local server. Use a configuration that’s clearly dev-only:
<!-- Info.Debug.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
Wire via build configs so the dev Info.plist is only used for Debug builds. Release Info.plist has no exceptions. This is auditable in CI: assert that the release-config-compiled Info.plist contains no ATS exceptions other than the documented production set.
ATS and pinning
ATS validates that the server’s cert chain meets the cipher and version requirements. It does not pin — chain validation succeeds for any system-trusted CA. Pinning (Chapter 9.3) is layered on top via URLSessionDelegate. The two work together:
- ATS: ensures the connection properties are strong
- Pinning: ensures the server identity is what you expect
Disabling ATS for a domain doesn’t disable your pinning — they’re independent. Conversely, having ATS doesn’t mean you’re pinned. Belt and suspenders.
What’s new since iOS 14+
NSRequiresCertificateTransparencywas added — by defaultfalse, but settruefor sensitive endpoints to require CT log proofs- TLS 1.3 is the negotiated default when both endpoints support it; ATS minimum remains 1.2
NSAllowsArbitraryLoadsInWebContentwas scoped more strictly — now requires a specific App Store Review attestation
Audit script for CI
# fail the build if Info.plist contains forbidden ATS keys (release config only)
INFO_PLIST="$BUILT_PRODUCTS_DIR/$INFOPLIST_PATH"
FORBIDDEN=$(/usr/libexec/PlistBuddy -c "Print :NSAppTransportSecurity:NSAllowsArbitraryLoads" "$INFO_PLIST" 2>/dev/null)
if [ "$FORBIDDEN" = "true" ]; then
echo "error: NSAllowsArbitraryLoads is set in release build"
exit 1
fi
Wire into a Run Script build phase active only for the Release configuration.
In the wild
Most banking and healthcare apps have zero ATS exceptions in production — their backends are fully TLS-modern. Consumer apps that integrate with third-party legacy ad networks accumulate exceptions over time; the Twitter/X iOS app’s Info.plist has historically included a few documented domain exceptions for ad partners. Browsers (Brave, DuckDuckGo) explicitly use NSAllowsArbitraryLoadsInWebContent with App Store Review approval.
Common misconceptions
- “ATS = certificate pinning.” Different things. ATS validates connection properties; pinning validates server identity. Use both.
- “
NSAllowsArbitraryLoadsis fine if we ‘know what we’re doing’.” App Store Review disagrees. Even when accepted, it’s an audit red flag forever. - “ATS doesn’t apply to third-party SDKs.” It applies to all CFNetwork-based requests, including from SDKs you ship. Audit dependencies’ network behavior; some old SDKs make HTTP requests that silently fail under ATS.
- “Setting
NSExceptionMinimumTLSVersionlower is just a slight relaxation.” TLS 1.0 has known attacks (BEAST, POODLE). Any downgrade is a real security regression. - “
NSAllowsLocalNetworkingis for development.” No — it’s for production apps controlling devices on the local network. For dev, use a Debug-config Info.plist with alocalhostexception.
Seasoned engineer’s take
ATS is the most successful security default in modern iOS. It single-handedly forced the entire mobile ecosystem to TLS 1.2+. Treat exceptions as commitments — every entry requires a comment in the Info.plist (yes, XML comments work) explaining why this domain, this key, until when. Quarterly, walk the exception list and ask “can we remove this yet?” Most can, eventually. Apps without exception hygiene accumulate cruft that auditors point at five years later.
TIP: For brand-new projects, target zero ATS exceptions on day one. Modern backends, even legacy vendors, almost always support TLS 1.2 with FS — you just have to ask the vendor’s support team to enable it.
WARNING: Don’t add
NSAllowsArbitraryLoads“temporarily” for a sprint. Temporary becomes permanent. Add domain-specific exceptions with a comment noting the planned removal date, then track them.
Interview corner
Junior: “What is App Transport Security?”
iOS networking enforcement that requires HTTPS with TLS 1.2+, strong ciphers, and forward secrecy by default. Exceptions are configured via NSAppTransportSecurity in Info.plist.
Mid: “When would you use NSAllowsArbitraryLoads?”
Almost never. The only legitimate uses are general-purpose browsers and apps that process user-supplied URLs. Anything else should use scoped NSExceptionDomains. Global NSAllowsArbitraryLoads triggers App Store Review pushback and is a perpetual audit finding.
Senior: “Walk me through ATS strategy for a fintech app with one legacy partner integration.”
Production Info.plist has exactly one domain exception, scoped to api.partner-legacy.example.com, with NSIncludesSubdomains = false and the minimum specific relaxation (probably NSExceptionRequiresForwardSecrecy = false if their cipher suite is dated). Comment in the plist references the JIRA ticket tracking the partner’s TLS modernization with a target date. CI build script asserts no other ATS keys in the release Info.plist. Dev Info.plist (Debug-only build config) adds localhost and any staging hostnames. Quarterly review walks the exception list and pings partner contacts on aged exceptions. Pinning is layered separately on the auth/transaction endpoints — ATS handles connection properties, pinning handles identity. The whole policy fits in a one-pager that ships with the repo for new-engineer onboarding.
Red-flag answer: “We turned ATS off because it was annoying.” Reveals a culture of bypassing safety defaults; expect bigger gaps elsewhere.
Lab preview
Lab 9.1 includes auditing the starter app’s Info.plist for ATS misconfigurations and adding a CI script that fails the build on NSAllowsArbitraryLoads.