iOS Security Best Practices: Production-Grade App Security Implementation Guide (2026)
A production iOS security guide for 2026. Covers NSFileProtectionComplete, Keychain with biometric access control, Face ID/Touch ID implementation, local-first as a compliance strategy, certificate pinning, ATS configuration, and a production security audit checklist.
Most iOS security vulnerabilities in production apps are not sophisticated exploits. They are basic errors made in the first hours of architecture: sensitive data written to the wrong location, credentials stored in UserDefaults, network calls made without transport security, and encryption never applied because nobody asked for it explicitly.
This guide covers the security controls every production iOS app should implement, how to implement them correctly in Swift, and a checklist you can use before App Store submission or a compliance audit.
Data Protection: Encrypting Files at Rest
iOS encrypts device storage automatically, but the level of protection depends on the file protection class you assign. Getting this wrong leaves sensitive data accessible when the device is locked.
File Protection Classes
| Class | Key available | Appropriate for |
|---|---|---|
| NSFileProtectionComplete | Only when unlocked | Health records, financial data, credentials, PII |
| NSFileProtectionCompleteUnlessOpen | When unlocked or file opened before lock | Large media files that need background writing |
| NSFileProtectionCompleteUntilFirstUserAuthentication | After first unlock since reboot | Default for most app data — moderate protection |
| NSFileProtectionNone | Always | Non-sensitive files that need background access at all times |
For sensitive data, always use NSFileProtectionComplete. Default behavior (CompleteUntilFirstUserAuthentication) keeps encryption keys available after first unlock — which means once the user opens the device once, the keys remain accessible until reboot. For health records, payment data, or personally identifiable information, that is insufficient.
Setting File Protection in Code
// When creating a file
let attributes: [FileAttributeKey: Any] = [
.protectionKey: FileProtectionType.complete
]
try FileManager.default.createFile(
atPath: filePath,
contents: data,
attributes: attributes
)
// For a Core Data store
let storeDescription = NSPersistentStoreDescription(url: storeURL)
storeDescription.setOption(
FileProtectionType.complete as NSObject,
forKey: NSPersistentStoreFileProtectionKey
)
Set protection at file creation time. Changing it after the fact is possible but error-prone.
SwiftData and Core Data
When using SwiftData or Core Data with a persistent store, explicitly configure the protection class on your store description. The default is CompleteUntilFirstUserAuthentication.
Keychain: Secrets, Tokens, and Credentials
The iOS Keychain is the correct storage location for any secret: API tokens, session credentials, private keys, passwords, and cryptographic material. It is hardware-backed on Secure Enclave devices and encrypted separately from the file system.
Never use UserDefaults, Core Data, or flat files for secrets. UserDefaults is a plist stored in the app's container — accessible to anyone with a jailbroken device or a backup without encryption.
Storing and Retrieving from the Keychain
import Security
// Store a value
func storeSecret(_ value: String, forKey key: String) throws {
guard let data = value.data(using: .utf8) else { return }
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
guard status == errSecSuccess else {
throw KeychainError.unableToStore(status)
}
}
// Retrieve a value
func retrieveSecret(forKey key: String) throws -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
guard status == errSecSuccess, let data = result as? Data else { return nil }
return String(data: data, encoding: .utf8)
}
Use kSecAttrAccessibleWhenUnlockedThisDeviceOnly for credentials that should not transfer to a new device via backup. Use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly only when you need background access after first unlock.
Keychain with Biometric Access Control
For high-value secrets, require Face ID or Touch ID to retrieve them:
import LocalAuthentication
let access = SecAccessControlCreateWithFlags(
nil,
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
.biometryCurrentSet,
nil
)!
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: "high_value_secret",
kSecValueData as String: secretData,
kSecAttrAccessControl as String: access
]
SecItemAdd(query as CFDictionary, nil)
With .biometryCurrentSet, if the user changes their enrolled biometrics (adds a fingerprint or re-enrolls Face ID), the item becomes permanently inaccessible. This is the correct behavior for high-security credentials. Use .biometryAny if you need to survive biometric enrollment changes.
Face ID and Touch ID: LAContext
Use LAContext to perform biometric authentication before displaying or acting on sensitive information:
import LocalAuthentication
func authenticateWithBiometrics() async throws {
let context = LAContext()
var error: NSError?
guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
throw AuthError.biometricsUnavailable
}
try await context.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to access your health data"
)
}
Use .deviceOwnerAuthentication instead of .deviceOwnerAuthenticationWithBiometrics to allow passcode fallback. For apps handling financial or health data, always provide a passcode fallback path — biometrics can fail legitimately (wet hands, Face ID at unusual angles).
The localizedReason string is displayed to the user in the system dialog. Be specific. "Unlock your account" is weaker than "Confirm your identity to view account balances." App reviewers and security-conscious users notice.
Local-First Architecture as a Security Strategy
The most underused iOS security control is not an API. It is an architectural decision: keep sensitive data on the device and never transmit it to a server.
Local-first architecture means:
- No server to breach. Data that never leaves the device cannot be exfiltrated from a cloud database.
- No network transmission. Data never moves across a network in transit, eliminating a category of interception risk.
- No third-party data processors. No cloud vendor means no data processing agreements, no vendor security audits, and no exposure from your vendor's breach.
- Simpler compliance. For HIPAA, GDPR, and ISO 27001, local-first architecture eliminates the largest class of controls — server security, encryption in transit, cloud access control — from your compliance scope.
This pattern is covered in depth in the local-first iOS development guide. For apps handling health records, payment data, legal documents, or PII, the question is not "should we go local-first?" but "what is the minimum server footprint we actually need?"
Optional CloudKit sync — where users sync their own data through their own Apple ID — is often the right answer. Your servers never touch user data. iCloud's security model and Apple's compliance posture handle the cloud layer.
Network Security: ATS and Certificate Pinning
App Transport Security
iOS enforces App Transport Security (ATS) by default. All network connections must use HTTPS with TLS 1.2 or higher. ATS is not optional — disabling it requires explicit entitlement justification and App Store review.
Never add blanket ATS exceptions (NSAllowsArbitraryLoads: true) in production. If you need to connect to a specific non-HTTPS endpoint, add a scoped exception for that domain only. Blanket exceptions disable TLS enforcement for all connections.
Review your Info.plist ATS configuration before submission. App Store reviewers and automated analysis tools check for broad ATS exceptions.
Certificate Pinning
Certificate pinning adds a second layer of verification beyond the TLS certificate chain. Your app refuses connections to servers that don't present a specific certificate or public key, even if those servers have a valid CA-signed certificate.
Implement pinning using URLSession and URLSessionDelegate:
class PinningDelegate: NSObject, URLSessionDelegate {
let pinnedPublicKeyHash = "your-base64-sha256-public-key-hash"
func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust,
let certificate = SecTrustCopyCertificateChain(serverTrust)
.map({ $0 as? [SecCertificate] })?.first?.first
else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Extract and compare public key hash
// ... implementation depends on your pinning approach
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
}
Pin public keys rather than certificates. Public keys change less frequently. Always pin two keys — your active key and a backup — and test your rotation procedure before deploying pinning to production.
Certificate pinning is most important in apps handling financial transactions, health data, or credentials where a man-in-the-middle attack would have serious consequences.
Code-Level Security Practices
Disable screenshot capture for sensitive screens:
override var prefersPointerLocked: Bool { true }
// For SwiftUI — place this on views showing sensitive data
.background(PrivacyShieldingView())
// Simpler approach: use UITextField with isSecureTextEntry
For finance and health apps, set the privacy-sensitive background view approach — when the app moves to the background, sensitive content should be replaced with a placeholder to prevent exposure in the app switcher.
Jailbreak detection: Detect jailbroken devices if your compliance requirements demand it. Common signals include the presence of Cydia, writable /private, or the ability to fork a process. These checks are not foolproof — they are a deterrent and a signal, not a hard control.
Binary protection: Enable all Xcode hardened runtime flags. Use Stack Protection and Position Independent Executable settings. These are enabled by default in modern Xcode — verify they haven't been disabled.
Logging: Never log sensitive data. Sanitize log statements before production builds. print() and os.log() output is visible to anyone who connects the device to a Mac. Use build flags to strip debug logging from release builds.
iOS Security Audit Checklist
Before App Store submission or a compliance review:
Data at rest:
- [ ] All sensitive files use
NSFileProtectionComplete - [ ] No secrets stored in UserDefaults
- [ ] No secrets stored in Core Data or flat files
- [ ] API tokens and credentials in Keychain with appropriate accessibility flags
- [ ] Keychain items configured with
ThisDeviceOnlyto prevent backup transfer
Authentication:
- [ ] Biometric authentication implemented with
LAContext - [ ] Passcode fallback path available
- [ ]
localizedReasonstrings are specific and accurate - [ ] Session tokens stored in Keychain, not in memory or UserDefaults
Network:
- [ ] ATS enabled — no blanket
NSAllowsArbitraryLoads - [ ] Certificate or public key pinning implemented for all API endpoints
- [ ] TLS 1.2+ enforced on all connections
- [ ] No plaintext credentials sent over any connection
Code quality:
- [ ] No sensitive data in log output
- [ ] No secrets hardcoded in source code
- [ ] Background/screenshot privacy protection on sensitive views
- [ ] Hardened runtime settings verified in build settings
Architecture:
- [ ] Minimum necessary server footprint — local-first where feasible
- [ ] CloudKit sync over custom server for user data when server needed
- [ ] Third-party SDK audit — what data do third-party libraries collect?
- [ ] Privacy nutrition label accurate and complete
App Store and compliance:
- [ ] Privacy policy covers every data type collected
- [ ] NSHealthShareUsageDescription and similar strings are accurate and specific
- [ ] No regulatory claims (medical diagnosis, financial advice) without appropriate licensing
Related Reading
- Local-First iOS App Development Guide — the architectural pattern that eliminates server-side security risk by design
- HealthKit Integration Guide: iOS Health App Development — HIPAA, GDPR, and privacy compliance for health app data
- iOS App Architecture Audit: 12 Critical Issues — a systematic review of production architecture problems that affect security and compliance
- Software Security Best Practices: Complete Development Guide — broader security practices across server, API, and web application layers
- iOS App Development Cost Breakdown — planning and budgeting for security controls and compliance work
Frequently Asked Questions
Does on-device storage mean data is automatically secure?
No. On-device storage must be configured with the correct Data Protection class. The default (CompleteUntilFirstUserAuthentication) keeps encryption keys available after first unlock. Set NSFileProtectionComplete explicitly on files containing sensitive data.
Should sensitive credentials always be stored in the Keychain?
Yes. The Keychain is encrypted, hardware-backed on Secure Enclave devices, and can require biometric authentication. Never use UserDefaults, flat files, or Core Data for secrets.
Does certificate pinning break when the certificate renews?
It can. Pin public keys rather than certificates — they change less frequently. Always pin a backup key alongside your primary key, and test your rotation procedure before deploying pinning to production.
What does local-first mean for iOS security compliance?
Local-first means data never leaves the device unless the user explicitly syncs. No server to breach, no network transmission to intercept, no third-party cloud processors. It eliminates the largest class of server-side compliance controls from your audit scope.
When should you perform an iOS security audit?
Before first production release, before any compliance certification (HIPAA, ISO 27001, SOC 2), after significant architecture changes, and when adding new sensitive data handling. Security audits are far cheaper than breach response.
Security is built in, not added on. If you need a production-grade iOS app with a defensible security architecture from day one, learn more at 3nsofts.com.