Skip to main content
All articles

iOS App Security for Businesses: What Actually Protects Your Data

Ben Van AkenCo-Founder & CTO10 min read

iOS has the best security defaults of any consumer platform you can build a business app on. That is genuinely true, and it is also why so many iOS apps are less secure than their owners believe: the platform handles enough that teams stop thinking about the boundary where it hands responsibility back.

This is a map of that boundary — what the operating system guarantees, what it offers only if you ask correctly, what it cannot do at all, and what an auditor will want evidence of when your customer's procurement team sends over a security questionnaire. Worth saying up front that only two of the five things you are defending against are really iOS problems. A lost device and a network attacker are platform work. Abuse by a hostile user of your own app, reverse-engineering of your binary, and what your third-party SDKs are quietly collecting are not.

Where should credentials and tokens actually live?

In the Keychain, with an explicit accessibility class — never in UserDefaults, a plist, Core Data or a file you wrote yourself.

UserDefaults is a property list in your app container: included in unencrypted backups unless excluded, readable by anyone with file access to a jailbroken device. It is a preferences store, not a secret store. The Keychain is an OS-managed database encrypted with keys derived from the device's hardware key and, depending on the class you pick, the user's passcode. That class decides what an attacker with a locked stolen phone can read:

  • kSecAttrAccessibleWhenUnlocked — readable only while the device is unlocked. The right default for anything used in the foreground.
  • kSecAttrAccessibleAfterFirstUnlock — readable after the first unlock following a boot, including while locked afterwards. Needed for background tasks and push handlers, and weaker: a device seized while running has already had its first unlock.
  • kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly — the strongest commonly usable option. Requires a passcode, never leaves the device, never restored onto a different phone.
  • The ThisDeviceOnly suffix generally. Use it for anything that should not appear on a new device restored from this user's iCloud backup — for session tokens, that is usually what you want.

Add a SecAccessControl when the value deserves a second gate — biometrics or the passcode at the moment of use, rather than merely an unlocked device:

Swift
var error: Unmanaged<CFError>?
let access = SecAccessControlCreateWithFlags(
    nil,
    kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
    [.biometryCurrentSet],
    &error
)

var query: [String: Any] = [
    kSecClass as String: kSecClassGenericPassword,
    kSecAttrService as String: "com.acme.refresh",
    kSecAttrAccount as String: accountID,
    kSecValueData as String: tokenData,
]
if let access {
    query[kSecAttrAccessControl as String] = access
}
let status = SecItemAdd(query as CFDictionary, nil)

The .biometryCurrentSet flag invalidates the item when enrolled fingerprints or faces change — that is the point, and it means handling the item vanishing without locking the user out permanently. Note too that biometric evaluation is a user-presence check, not authentication to your server: treat it as re-authorisation of a token you already hold, never as proof of who is holding the phone.

What does the Secure Enclave actually do for you?

It holds private keys your code can use but never read, which lets you sign things in a way that is hard to lift off the device.

Generate a key with kSecAttrTokenIDSecureEnclave and the private material is created inside a separate coprocessor and stays there. Your app asks for a signature; it does not get the key. An attacker who fully compromises iOS still cannot copy it to their own machine, which turns an unlimited, silent, remote compromise into a limited, local one. Use it for device-bound session keys, so a stolen refresh token is useless without the phone it was issued to; for signing high-value requests such as an approval or payment instruction; and for passkeys, where the platform does the work and phishing-resistant authentication falls out as a side effect. If you still ship password login, passkeys are probably the highest-value security change available to you.

What protects data at rest on the device?

File-level encryption called Data Protection, which is on by default but at a weaker class than most teams assume.

The default for new files is NSFileProtectionCompleteUntilFirstUserAuthentication: encrypted at rest, but decryptable once the user has unlocked after boot — which describes a phone that has been in someone's pocket all day. For genuinely sensitive files, set NSFileProtectionComplete, which makes them unreadable while the device is locked.

That trade-off bites in production, because a completely protected file cannot be read by a background task or a silent push handler running while the phone is locked. Teams meet this as a crash-on-lock bug and "fix" it by downgrading protection everywhere. The correct fix is to split the data: an unprotected queue background work can touch, and the sensitive payload behind complete protection until the user is present. Four related things get missed:

  • Backups. Anything not excluded goes to iCloud or a local backup. Mark caches and sensitive derivatives with isExcludedFromBackup, and note that encrypted local backups preserve Keychain items while unencrypted ones do not.
  • Logs and crash reports. The fastest way to leak a token is to print it. Audit what your logging framework and crash reporter capture, and check OSLog privacy annotations are not public on user-specific values.
  • The app-switcher snapshot. iOS screenshots your app as it backgrounds. If account numbers are on screen, cover the window in sceneWillResignActive. Cheap fix, repeat pen-test finding.
  • The passcode itself. These keys derive partly from it, so none of the above means much on a device without one. Enforce it through MDM rather than having your app plead for it.

How do you stop something that isn't your app from calling your API?

With App Attest, which gives your server cryptographic evidence that a request came from a genuine, unmodified build of your app on genuine Apple hardware.

The old answer to that question was a shared secret compiled into the binary, which is no answer at all, because the binary is in the attacker's hands. App Attest works in two phases instead: once per install the app generates a hardware-backed key and produces an attestation your server verifies with Apple's anonymous attestation service; after that, each sensitive request carries a short assertion signed over a server-issued challenge, validated against the stored public key and a rising counter.

Swift
let service = DCAppAttestService.shared
guard service.isSupported else {
    // Simulator, and some older hardware. Fall back to
    // a server-side risk decision — never to trusting
    // the client because attestation was unavailable.
    return try await registerWithoutAttestation()
}

let keyID = try await service.generateKey()
let attestation = try await service.attestKey(
    keyID,
    clientDataHash: Data(SHA256.hash(data: challenge))
)
try await api.register(keyID: keyID,
                       attestation: attestation)

Bot traffic, cloned clients, credential stuffing at scale and free-tier abuse all get much more expensive, because every fake identity now needs real hardware. What it does not buy you is protection from a legitimate, attested app on a device the attacker controls: jailbreak the phone, instrument the real app, and the attestation stays valid, because it is real. App Attest answers "is this my app on Apple hardware", not "is this user honest". DeviceCheck is the smaller sibling — two bits of per-device state that survive reinstalls, plus an Apple risk signal. Use it to stop one device farming your free trial; do not mistake it for authentication.

Is certificate pinning still worth doing?

Sometimes. App Transport Security already covers the common case, and pinning adds a narrow protection alongside a real operational risk.

ATS is on by default and enforces modern TLS — 1.2 or better, forward secrecy, a trusted root. Global exceptions may have to be justified to App Review, and "our legacy endpoint doesn't support it" is a reason to fix the endpoint. Get ATS clean and most passive network risk is gone. Pinning goes further: this connection must present a chain containing a key we specifically expect, so a trusted-but-not-ours CA cannot intercept it. That defeats casual inspection with an interception proxy, which is where most people poking at your API start.

The costs deserve stating plainly. Pin the wrong thing and every installed copy breaks the day your certificate rotates, and the fix needs a release, a review and users updating. So: pin public keys rather than certificates, ship a backup pin for your next key, prefer Info.plist NSPinnedDomains over a hand-written URLSessionDelegate, and keep a tested kill switch. Accept too that pinning is bypassable on a device the attacker controls. It raises effort, and effort is the currency here.

What does jailbreak detection actually get you?

A signal, not a control. Every jailbreak check that has ever shipped can be defeated, usually with an off-the-shelf tool and no source code.

That deserves saying bluntly, because vendors sell the opposite. Detection looks for suspicious paths, tries to write outside the sandbox, or inspects loaded libraries — all inside your process, and on a jailbroken device the attacker owns the process. Hooking frameworks routinely patch the check to return whatever answer the app wants, and there are maintained, freely available tools whose whole purpose is doing that to popular apps. Use it the way it works:

  • Report it, do not rely on it. Send the signal to the server as one input into a risk score alongside App Attest state, velocity and behaviour. Decisions belong where the attacker cannot patch them.
  • Do not hard-block on it alone unless a regulator requires it. False positives generate support load, and the users you want to stop are the ones who bypass it.
  • Never let it be the only thing between a user and money, data or an entitlement. If the answer to "what if this check lies" is a breach, the architecture is wrong.
  • Pair it with controls that hold: server-side authorisation, device-bound Secure Enclave keys, short token lifetimes, revocation you can execute.

Any security decision made on a device you do not control is a suggestion.

What do Apple's privacy requirements demand now?

An accurate, declared account of what your app and every SDK inside it collects — plus a stated reason for using certain ordinary APIs.

Three obligations get conflated. Privacy nutrition labels are your public declaration of what you collect and how it links to the user. App Tracking Transparency governs the prompt shown before tracking a user across other companies' apps. The privacy manifest — a PrivacyInfo.xcprivacy file in your bundle — declares collected data types, tracking domains, and your reasons for calling a set of APIs Apple flagged as commonly abused for fingerprinting: file timestamps, boot time, free disk space, the active keyboard list, UserDefaults. Apple also requires manifests and signatures for a published list of commonly used third-party SDKs, which is the point of the exercise — your privacy label is only as truthful as the SDKs nobody has read.

Operationally: keep a dependency inventory with an owner and a purpose per SDK, and delete anything nobody can justify. Most inaccurate privacy labels are not deception — they are an analytics SDK added years ago that no one has thought about since.

How do you secure an app across a managed fleet?

Through MDM and managed app configuration, which move policy out of your app and into infrastructure the customer's IT team already runs.

Enrolled devices can be required to carry a passcode of given complexity, stay up to date, and be remotely wipeable — and a managed app's data can be wiped independently of personal data on a BYOD phone. That last capability is often the deciding factor for a customer's security team, and supporting it costs you nothing. Managed app configuration is the underused half: an MDM pushes a settings dictionary into your app, read from UserDefaults under the com.apple.configuration.managed key. It delivers a per-tenant API endpoint, an SSO issuer URL or a feature policy without anyone typing a server name on 400 devices.

If you sell into enterprises, note that Apple supports private distribution of custom apps to specific organisations through Apple Business Manager, keeping a client's internal tool off the public store. It still goes through App Review, so budget the time either way.

What do SOC 2 and ISO 27001 auditors actually ask about a mobile app?

Far less about cryptography than you expect, and far more about who can ship a build — because those frameworks assess whether the controls you claim are real, documented and consistently applied. Expect requests for evidence on questions like these:

  • Who can push to the release branch, who approves it, and can you show merged pull requests reviewed by someone other than the author?
  • Who has access to App Store Connect and the Apple Developer account, at what role, and when was that list last reviewed? Ex-employees still holding an Admin role is a common finding.
  • Where do signing certificates live and who can use them? A distribution certificate on somebody's laptop is a worse problem than anything in your app code.
  • How do secrets reach CI, how are they rotated, and what shows they are not in the repository?
  • How do you know what is in your app — dependency inventory, vulnerability scanning, and a process for acting on findings?
  • Can you revoke a session? If an employee leaves at 14:00, show how the app loses access, and how fast.
  • When was the last penetration test, what did it find, and what evidence is there of remediation?

So compliance is mostly a delivery-process decision, and far cheaper made at the start. Branch protection, secrets in a managed store and a written release approval cost days in month one, and a great deal more retrofitted under pressure during a customer's security review. One last thing, worth doing before somebody else does it for you:

Bash
# Unzip a .ipa, then look at the Mach-O binary.
BIN="Payload/Acme.app/Acme"
strings "$BIN" | grep -iE 'api_?key|secret|passwd'
strings "$BIN" | grep -iE 'https?://[a-z0-9.-]+'

Anything printable there is public. That is not an argument for obfuscation; it is an argument for it not being a secret — swap embedded keys for tokens your server issues to an authenticated, attested client.

What does a reasonable baseline look like?

  1. Enforce every authorisation rule on the server, and assume the client will lie.
  2. Tokens in the Keychain with an explicit ThisDeviceOnly class, refresh tokens bound to a Secure Enclave key, and a revocation path you have tested.
  3. ATS clean with no global exceptions. Pin only if you will also maintain backup pins and a kill switch.
  4. App Attest on the endpoints that create value or cost money, with a server-side fallback when it is unavailable.
  5. NSFileProtectionComplete on sensitive files, a deliberate backup-exclusion policy, no secrets in logs or crash reports.
  6. Passkeys or SSO instead of passwords, and MDM-managed configuration if you sell to companies.
  7. A dependency inventory, a current privacy manifest, a nutrition label that is true, and access reviews on a calendar.

The short version

iOS will protect a lost device for you. It will not protect you from your own architecture: the platform's guarantees cover data at rest, transport and the identity of your binary, not authorisation, abuse, or a secret you compiled into your bundle. So be honest about which of your controls are hard and which are speed bumps. Keychain classes, Secure Enclave keys, server-side authorisation and App Attest are hard. Jailbreak detection, obfuscation and client-side checks are speed bumps. Confusing one for the other is how apps get breached while their security section reads beautifully.

Need a second opinion on your app's security?

We build native iOS for teams with real compliance obligations, and we're happy to review what you already have. Book a call and we'll go through the threat model, the gaps, and what is worth fixing first.