App Transport Security Best Practices on iOS
In the world of mobile app development, security is paramount. Users trust our apps with their data, and it's our responsibility to protect it. A critical component of this security on Apple platforms is App Transport Security (ATS). Introduced in iOS 9, ATS significantly enhances user privacy and data integrity by enforcing secure network connections by default.
But what exactly is ATS, and how does it impact your app? More importantly, what are the best practices for working with it, especially when dealing with legacy systems that might not yet meet modern security standards? This article will dive deep into ATS, providing you with the knowledge and practical steps to ensure your app's network communications are secure and compliant.
Understanding App Transport Security (ATS)
At its core, ATS is a set of policies that dictate how your app can connect to web services. Since iOS 9, watchOS 2, tvOS 9, and macOS 10.11, ATS has been enabled by default for all new apps. This means that all HTTP connections made by your app must conform to specific security requirements.
Specifically, ATS requires:
- HTTPS (TLS 1.2 or higher): All connections must use HTTPS. This encrypts data in transit, preventing eavesdropping and tampering.
- Forward Secrecy: The server must support Forward Secrecy, which ensures that even if a server's long-term private key is compromised, past communications remain secure.
- Strong Cipher Suites: Only modern, strong cryptographic cipher suites are allowed.
- Trusted Certificates: Server certificates must be signed by a trusted Certificate Authority (CA) and meet specific size requirements (e.g., RSA 2048-bit or greater, ECDSA 256-bit or greater).
If your app attempts to connect to a server that doesn't meet these criteria, the connection will fail, resulting in an error similar to App Transport Security has blocked a cleartext HTTP (http://) resource load because it is insecure.. This aggressive stance by Apple is a clear signal: secure your network communications.
The Default ATS Behavior
By default, any network request originating from your app using URLSession (or other higher-level APIs like WebKit) will be subject to ATS scrutiny. If the destination server does not adhere to the strict ATS requirements, the connection will be blocked. This is a good thing for security, as it protects users from potential Man-in-the-Middle (MitM) attacks, data interception, and other vulnerabilities associated with insecure HTTP connections.
For most modern applications that communicate with well-maintained APIs, ATS should not be an issue. However, challenges arise when integrating with older, third-party services, legacy internal APIs, or content delivery networks (CDNs) that might not yet have fully adopted modern security standards.
When and How to Bypass ATS (Carefully!)
While Apple strongly discourages disabling ATS, they provide mechanisms to create exceptions when absolutely necessary. These exceptions are configured in your app's Info.plist file, under the NSAppTransportSecurity dictionary. It's crucial to understand that bypassing ATS should be a last resort and done with extreme caution, always aiming to secure the backend service itself rather than weakening your app's security posture.
Here are the primary ways to configure ATS exceptions, from least to most recommended:
1. NSAllowsArbitraryLoads (The "Nuclear Option")
This boolean key, when set to YES, completely disables ATS for all domains. This is highly discouraged by Apple and should never be used in production apps. It opens up your app and users to significant security risks, allowing connections over insecure HTTP and to servers with weak TLS configurations.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/> <!-- AVOID THIS IN PRODUCTION! -->
</dict>
If you ever find this key set to true in your Info.plist, you should make it a top priority to remove it and adopt more granular exceptions or, ideally, migrate your backend to secure HTTPS.
2. NSAllowsArbitraryLoadsForMedia
This key is a slightly less dangerous version of NSAllowsArbitraryLoads, allowing arbitrary loads specifically for media content (e.g., video and audio streams). It's intended for scenarios where you need to play media from various sources, some of which might be insecure, but your core data communications remain protected by ATS.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsForMedia</key>
<true/>
</dict>
While better than a full arbitrary load, still use this with caution and only for media content.
3. NSExceptionDomains (The Recommended Approach)
This is the preferred and most secure way to bypass ATS for specific domains. NSExceptionDomains is a dictionary where each key is a domain name (e.g., api.legacy.com), and its value is another dictionary containing specific exception rules for that domain. This allows you to apply exceptions only where they are strictly necessary, keeping the rest of your app's connections secure.
Here are the common keys you can use within an NSExceptionDomains entry:
NSExceptionAllowsInsecureHTTPLoads(Boolean): Set totrueif the domain serves content over plain HTTP. This is often the primary reason for needing an exception.NSExceptionMinimumTLSVersion(String): Specifies the minimum TLS version required for this domain (e.g.,TLSv1.0,TLSv1.1,TLSv1.2). Use this if a server cannot meet TLS 1.2.NSExceptionRequiresForwardSecrecy(Boolean): Set tofalseif the domain does not support Forward Secrecy. By default, even exceptions require Forward Secrecy.NSIncludesSubdomains(Boolean): Set totrueif the exception should also apply to all subdomains of the specified domain (e.g.,sub.api.legacy.com).
Practical Example: Configuring NSExceptionDomains
Let's say your app needs to connect to http://legacy.api.example.com which only supports HTTP and an older TLS version (e.g., TLS 1.0) without Forward Secrecy, and also https://another.api.example.com which is HTTPS but also lacks Forward Secrecy.
Your Info.plist would look something like this:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>legacy.api.example.com</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>another.api.example.com</key>
<dict>
<key>NSExceptionRequiresForwardSecrecy</key>
<false/>
<key>NSIncludesSubdomains</key>
<true/> <!-- Applies to any.another.api.example.com too -->
</dict>
</dict>
</dict>
With this configuration, only legacy.api.example.com and another.api.example.com (and its subdomains) will have relaxed ATS rules, while all other network connections in your app will still enforce the strict default ATS requirements.
Swift Code Example for Network Request
Here's a simple URLSession example that demonstrates how ATS might affect your requests. If you try to access an http:// URL without proper NSExceptionDomains setup, it will fail.
import Foundation
func makeNetworkRequest(to urlString: String) async {
guard let url = URL(string: urlString) else {
print("Invalid URL: \(urlString)")
return
}
print("Attempting to connect to: \(urlString)")
do {
let (data, response) = try await URLSession.shared.data(from: url)
guard let httpResponse = response as? HTTPURLResponse else {
print("Not an HTTP response for \(urlString).")
return
}
if (200...299).contains(httpResponse.statusCode) {
print("✅ Successfully fetched data from \(urlString). Status Code: \(httpResponse.statusCode)")
// Optionally print data:
// print("Data: \(String(data: data, encoding: .utf8) ?? "N/A")")
} else {
print("❌ Request failed for \(urlString). Status Code: \(httpResponse.statusCode)")
print("Response headers: \(httpResponse.allHeaderFields)")
}
} catch {
print("❌ Network request failed for \(urlString) with error: \(error.localizedDescription)")
if let urlError = error as? URLError, urlError.code == .appTransportSecurityRequiresSecureConnection {
print("💡 This error is likely due to App Transport Security (ATS) blocking an insecure connection.")
print(" Consider updating your server to HTTPS or configuring ATS exceptions carefully in Info.plist.")
}
}
}
// To run this, you'd typically call it within a Task in an async context,
// for example, from a button action or in an `onAppear` modifier in SwiftUI.
// For demonstration, let's simulate calls:
/*
// Example Usage:
Task {
// This will likely succeed if the domain is secure and ATS compliant
await makeNetworkRequest(to: "https://api.github.com/zen")
// This will likely fail due to ATS if 'insecure.example.com' is HTTP
// and not configured in NSExceptionDomains
await makeNetworkRequest(to: "http://insecure.example.com/data")
// If 'legacy.api.example.com' is configured in NSExceptionDomains
// as shown above, this might succeed even if it's HTTP.
await makeNetworkRequest(to: "http://legacy.api.example.com/status")
}
*/
Checking ATS Compliance with nscurl
Before even writing a single line of Swift code, you can test a URL's ATS compliance using the nscurl command-line tool. This tool simulates a URLSession request and reports any ATS violations.
Open your Terminal and run:
nscurl --ats-diagnostics https://api.example.com
Replace https://api.example.com with the actual URL you want to test. The output will detail which ATS requirements are met or failed for the given URL. Look for lines like ATS Dictionary: (null) (meaning no exceptions needed for that URL) or specific failures (Result : FAIL). This is an invaluable tool for debugging ATS issues.
Best Practices and Recommendations
- Prioritize Securing Your Backend: The absolute best practice is to ensure all your backend services and third-party APIs use HTTPS with TLS 1.2+, strong cipher suites, and Forward Secrecy. This eliminates the need for any ATS exceptions in your app.
- Avoid
NSAllowsArbitraryLoads: Seriously, do not use this in production. It defeats the entire purpose of ATS and exposes your users to unnecessary risks. - Use
NSExceptionDomainsSparingly: If you must make an exception, useNSExceptionDomainsto target only the specific, non-compliant domains. Be as granular as possible. - Regularly Review
Info.plist: Periodically audit yourInfo.plistfor any ATS exceptions. As backend services are updated, you might be able to remove old exceptions, further strengthening your app's security. - Educate Your Team: Ensure all developers working on your project understand the importance of ATS and the implications of bypassing it.
- Test Thoroughly: Always test network connections on real devices and simulators with various configurations (including and excluding your ATS exceptions) to catch any unexpected behavior.
Info.plist
└─ NSAppTransportSecurity (Dictionary)
├─ NSAllowsArbitraryLoads (Boolean, NO by default, AVOID!)
└─ NSExceptionDomains (Dictionary)
└─ my.legacy.api.com (Dictionary)
├─ NSExceptionAllowsInsecureHTTPLoads (Boolean, YES if HTTP needed)
├─ NSExceptionMinimumTLSVersion (String, e.g., TLSv1.0)
├─ NSExceptionRequiresForwardSecrecy (Boolean, NO if not supported)
└─ NSIncludesSubdomains (Boolean, YES for subdomains)
Future of ATS
Apple's commitment to security is unwavering. While they haven't completely locked down ATS, their consistent encouragement to adopt secure connections suggests that future iOS versions might make it even harder to bypass ATS, possibly deprecating some of the exception keys. Staying ahead by securing your backend services is the most future-proof strategy.
Summary
App Transport Security is a fundamental security feature on Apple platforms that protects user data by enforcing secure network connections. While it can sometimes pose challenges when integrating with older services, understanding its mechanisms and adhering to best practices—primarily using NSExceptionDomains sparingly and striving for secure backends—will ensure your app remains robust and trustworthy. Prioritize security, and your users will thank you.
Happy Swifting!