The API remains accessible throughout testing, yet the archived build immediately reports a network error when handed to the QA team. The opposite scenario is even more dangerous: someone temporarily bypasses a certificate issue and leaves NSAllowsArbitraryLoads enabled in the release configuration. The app appears to work again, but every connection that ATS should have blocked is now allowed. Preventing both types of incidents requires inspecting not just the Info.plist in the repository, but the App actually compiled on the cloud Mac.
Define What the Gate Must Check
An actionable ATS gate should cover at least three layers: static configuration, host-side TLS handshakes, and application-level requests. Each layer answers a different question, so a single successful curl request cannot replace the full validation process.
| Layer | What to inspect | Check first on failure |
|---|---|---|
| Static configuration | Final Info.plist inside the App | Build configuration, script modifications, domain exceptions |
| TLS probe | Cloud Mac connection to the target endpoint | DNS, certificate chain, protocol version, redirects |
| Application request | Actual URLSession behavior | ATS, session configuration, authentication, and response parsing |
Start by defining the policy clearly: release artifacts must not contain NSAllowsArbitraryLoads; NSExceptionDomains may include only reviewed domains; temporary exceptions must identify an owner and removal criteria; and settings required for local debugging must never enter Release builds.
An ATS exception is not a general-purpose switch for “getting the network working.” It is a security change that requires a defined scope, a documented reason, and an exit plan.
Audit the Final Build Artifact
The plist in source control may be overridden by INFOPLIST_KEY_*, different xcconfig files, or build scripts. First complete a Release build, then locate and inspect the generated artifact:
set -euo pipefail
xcodebuild \
-scheme "$SCHEME" \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath "$PWD/.derived-data" \
build
APP_PATH="$(find "$PWD/.derived-data/Build/Products" \
-type d -name '*.app' -path '*Release-*' -print -quit)"
test -n "$APP_PATH"
PLIST="$APP_PATH/Info.plist"
plutil -lint "$PLIST"
plutil -extract NSAppTransportSecurity json -o - "$PLIST" \
> "$PWD/ats-effective.json" 2>/dev/null || printf '{}
' > "$PWD/ats-effective.json"
The gate should treat both an absent ATS dictionary and an empty dictionary as valid, not as errors. It should fail only for broad allowances or unapproved exceptions. The following script accepts the allowlist through an environment variable, avoiding hard-coded internal team domains in a public script:
import json
import os
import sys
with open(sys.argv[1], encoding="utf-8") as f:
ats = json.load(f)
if ats.get("NSAllowsArbitraryLoads") is True:
raise SystemExit("NSAllowsArbitraryLoads is forbidden")
approved = {
item.strip().lower()
for item in os.getenv("ATS_APPROVED_DOMAINS", "").split(",")
if item.strip()
}
exceptions = ats.get("NSExceptionDomains", {})
unknown = sorted(set(map(str.lower, exceptions)) - approved)
if unknown:
raise SystemExit("Unapproved ATS domains: " + ", ".join(unknown))
Run it with python3 ci/audit_ats.py ats-effective.json. CI logs may retain key names and check results, but they must not print request tokens, cookies, or complete authorization headers.
Maintain a Reviewable Exception Inventory
Comparing domain names alone is not enough. Each exception should document the permitted ATS keys, applicable environment, justification, and review or removal criteria. Pay particular attention to NSIncludesSubdomains: it expands the exception to every subdomain and must not be enabled by default merely because only one API is currently in use.
Keep the inventory as JSON or YAML in the repository, and verify the following during review:
- Domains must be exact; wildcard-style descriptions are not accepted;
- The TLS version must not be lowered below the project baseline;
- Do not allow an entire parent domain merely to accommodate a single redirect;
- Debug endpoints belong only in the Debug configuration;
- After removing an exception, rerun both the build and request tests.
Check for Configuration Leakage
Build Debug and Release separately, then export and compare two ats-effective.json files. The gate should fail immediately if Release contains keys intended only for traffic inspection or local services. Do not compare source files alone, because different build settings can inject different values into the same plist.
Probe TLS and the Redirect Chain
After the static audit passes, probe the target address from the cloud Mac that performs the build. nscurl produces an ATS diagnostic matrix that helps isolate protocol, certificate-chain, and forward-secrecy issues:
test -n "${API_URL:-}"
/usr/bin/nscurl --ats-diagnostics "$API_URL" \
> "$PWD/ats-diagnostics.txt" 2>&1
This output is useful for diagnosis, but the gate should not simply search the file for “PASS,” because diagnostic mode tests multiple relaxed combinations. CI should make its definitive decision by sending a controlled request to the project's actual URL with limits on timeouts, redirect count, and response codes:
curl --fail --silent --show-error \
--proto '=https' \
--tlsv1.2 \
--max-time 15 \
--max-redirs 3 \
--output /dev/null \
"$API_URL"
A successful curl request proves only that the host-side connection works. It does not apply the iOS App's ATS configuration, nor does it verify the app's session delegate, request headers, or authentication logic.
Add an Application-Level Regression Test
Finally, add a lightweight test target that uses the production networking stack. The test should read a URL injected by the test environment, perform a health check with URLSession, and assert that the request completes, the status code matches the contract, and no redirect leads to a non-HTTPS address. Never hard-code credentials in test code.
Preserve Enough Evidence, but No More Than Necessary
On failure, archive only the final ATS dictionary, the domain-inventory comparison result, the nscurl output, the request status code, the hostname of each redirect target, and the Xcode build configuration name. Certificate bodies, access tokens, and complete response bodies generally do not belong in long-term logs.
Fix the execution order as “Release build → final plist audit → TLS probe → URLSession test.” This sequence detects configuration drift while distinguishing an App policy rejection from changes to the target endpoint's certificate chain, DNS, or redirects.
Pre-Release Checklist
Before merging, confirm that the release artifact does not allow arbitrary network access, every domain exception appears in the approved inventory, subdomain scope has been explicitly reviewed, the TLS probe uses the real target address, and the application-level test uses the actual URLSession configuration. When a failure occurs, preserve the artifact and diagnostic files before changing any configuration. Do not mask certificate or redirect problems by adding another exception.
This gate is not intended to make every network failure disappear automatically. Its purpose is to isolate failures at an actionable layer and ensure that temporary debugging settings do not quietly enter the next release.
Frequently asked questions
Why inspect the built Info.plist instead of the source file?
Build settings, configuration files, and scripts can alter the final property list. The app bundle shows the configuration that will actually ship.
Does a successful nscurl diagnostic prove the app can connect?
No. It confirms host-level TLS capabilities, but the app still needs a URLSession test covering ATS policy, redirects, and authentication behavior.
Which ATS settings should fail the pipeline?
Reject NSAllowsArbitraryLoads and any domain exception absent from the approved allowlist. Debug-only exemptions must also be excluded from release artifacts.
Run your next development or build task on a cloud Mac mini
Choose from two M4 configurations, four rental periods, and five available locations. Actual availability is subject to the real-time status returned by the console.