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:

RequirementValue
Schemehttps:// only
TLS version≥ 1.2
Cipher suiteForward-secret (ECDHE, DHE)
CertificateRSA ≥ 2048-bit or ECC ≥ 256-bit
HashSHA-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)

KeyEffectAcceptable use
NSAllowsArbitraryLoadsDisables ATS globallyAlmost never. Red flag in audits.
NSAllowsArbitraryLoadsInWebContentDisables ATS only for WKWebViewApps that legitimately render arbitrary user-supplied web content (browsers, RSS readers)
NSAllowsArbitraryLoadsForMediaDisables ATS for AVFoundation media loadsApps streaming legacy HTTP video sources
NSAllowsLocalNetworkingAllows plaintext to .local, IP literals, unqualified hostnamesApps 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+

  • NSRequiresCertificateTransparency was added — by default false, but set true for sensitive endpoints to require CT log proofs
  • TLS 1.3 is the negotiated default when both endpoints support it; ATS minimum remains 1.2
  • NSAllowsArbitraryLoadsInWebContent was 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

  1. “ATS = certificate pinning.” Different things. ATS validates connection properties; pinning validates server identity. Use both.
  2. NSAllowsArbitraryLoads is fine if we ‘know what we’re doing’.” App Store Review disagrees. Even when accepted, it’s an audit red flag forever.
  3. “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.
  4. “Setting NSExceptionMinimumTLSVersion lower is just a slight relaxation.” TLS 1.0 has known attacks (BEAST, POODLE). Any downgrade is a real security regression.
  5. NSAllowsLocalNetworking is for development.” No — it’s for production apps controlling devices on the local network. For dev, use a Debug-config Info.plist with a localhost exception.

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.


Next: 9.9 — Privacy, Permissions & Data Minimization