Greetings, traveler!
Third-party code is a normal part of iOS development. Applications depend on Swift packages, binary SDKs, build tools, CI templates, and automation maintained outside the team.
These components are usually evaluated by their API, maintenance, performance, and effect on application size. Security review often focuses on the code that eventually runs on the user’s device.
That covers only part of the risk.
Some external code runs during development or inside CI. Depending on how it is executed, it may interact with source code, credentials, signing assets, and internal services. Supply chain security is about controlling that access and reducing the damage a compromised component can cause.
The supply chain is larger than the dependency graph
An iOS application’s supply chain includes source and binary dependencies, macros, package plugins, build scripts, automation tools, CI configuration, and signing infrastructure.
These components do not all present the same risk. What matters is where the code runs, which permissions it receives, and what is available in that environment.
A small code generator may require more scrutiny than a large UI library because the generator executes during the build.
Runtime and build-time code have different access
Code from a regular library target becomes part of the application. On the user’s device, it runs with the application’s entitlements and inside the iOS sandbox.
A compromised library can still access application data, modify behavior, or communicate over the network. Its access is limited by the permissions granted to the application.
Build-time components run in a different context.
Swift macro implementations execute during compilation, but they run in a restricted sandbox without file system or network access. Their main security risk is the code produced by their expansion and compiled into the application.
SwiftPM plugins are also sandboxed by default. Their file system and network access is limited. Command plugins can declare additional permissions, such as permission to modify files in the package directory.
Traditional shell scripts, Fastlane actions, code generators, CI commands, and executable targets invoked by the build process usually run as local processes with the permissions of the current user or runner. Depending on the environment, they may be able to read source files, environment variables, repository credentials, signing assets, or deployment secrets.
A package that provides build plugins, macros, binary targets, or executables invoked by the project needs a different review from a package that supplies only library code.
Review dependencies by access
Reviewing every dependency with the same depth is expensive and rarely useful. Instead, classify external components by what they can do:
- Does the code run only inside the application?
- Does it execute during compilation or packaging?
- Can it access the network?
- Can it modify files?
- Does it run in a job that receives secrets?
- Is its source available?
- Can an update change the build or release process?
A data structure library with no executable targets has a different risk profile from a plugin that downloads files or a script that changes generated source code.
The amount of review should match the component’s access and the sensitivity of the project.
Commit dependency lockfiles
Lockfiles make dependency changes visible and help reproduce builds. For an iOS application, this usually means committing files such as:
Package.resolvedPodfile.lockGemfile.lock
CI should build from the reviewed dependency state rather than update dependencies implicitly.
This helps prevent unexpected version changes and makes investigations easier. If a package is later found to be compromised, the team can identify which version was included in a release.
A lockfile records and stabilizes selected versions. It does not show whether those versions are safe.
Using an exact version requirement in Package.swift is not a replacement for a reviewed Package.resolved file. Exact requirements can make dependency resolution more fragile when several packages require different versions of the same dependency. For an application, it is usually better to use an appropriate semantic version range and keep the resolved version under version control.
An exact requirement may still be reasonable as a temporary containment measure, for example when later releases of a dependency are known to be affected by a regression or security issue. It should be treated as an explicit exception rather than the default dependency policy.
Review dependency updates separately
Dependency updates are easier to inspect when they are separated from feature work. A dedicated merge request makes it easier to notice changed revisions, new transitive dependencies, manifest changes, added plugins or executable targets, new binaries, and modified permissions.
The goal is not to audit every line of every release. Review should begin with changes in capability.
An update that adds a build plugin deserves more attention than one that only changes internal implementation, regardless of the version number.
Review build-time components separately
Macros, plugins, scripts, and executable tools need different types of review.
Macros should be checked for the code they generate. A small macro implementation can still expand into behavior that is difficult to notice at the call site.
Plugins, scripts, executables, and automation tools require additional scrutiny because they may interact with the build environment. For each tool, the team should know why it runs, which files it reads or modifies, and whether it needs network access.
Build tools that are not required for distribution should run before release credentials become available. A generator should not receive write access outside the directories it needs.
Large teams may also maintain an allowlist of approved build tools and plugins.
What a binary checksum verifies
SwiftPM uses checksums for remote binary targets. The checksum verifies that the downloaded archive matches the artifact referenced by the package manifest.
It does not verify the behavior of the binary.
If an attacker gains control of the vendor’s release process, they may be able to publish a new binary, update the checksum, and create a matching tag. The integrity check can still pass.
Sensitive projects may add further controls:
- an approved list of binary vendors
- internal mirrors for reviewed artifacts
- independently stored hashes
- verification of the vendor’s code signature and signing identity
- records connecting each binary to its source and release
- additional testing before SDK upgrades reach production
These measures improve traceability and make uncontrolled replacement harder. They still cannot prove that a binary is harmless.
Keep release secrets out of ordinary builds
No review process can guarantee that external code will never be compromised. CI should limit what ordinary build jobs can reach.
Build, lint, and test jobs usually do not need App Store distribution certificates, production provisioning profiles, App Store Connect API keys, deployment credentials, or organization-wide access tokens.
Release jobs should run separately. They can be restricted to protected branches or tags, require review, and use dedicated runners.
Secrets should be available only to jobs that need them. A test job that executes external build tools should not also have permission to publish the application.
Treat CI runners as sensitive machines
Many iOS teams use long-lived macOS runners because clean macOS environments are harder to create than Linux containers.
These runners may retain Keychain items, SSH keys, cached Git credentials, source checkouts, SwiftPM caches, DerivedData, Fastlane sessions, and temporary signing files.
Deleting the project workspace does not remove credentials, caches, Keychain items, or changes made elsewhere on the machine.
Separate system users provide a basic boundary between validation and release jobs. Separate runners provide stronger isolation, especially when release secrets or production access are involved.
More mature setups can restore runners from clean snapshots or create short-lived build machines for releases. Release credentials should be installed only when required and removed after the job completes.
The appropriate level of isolation depends on the project. A banking application needs stricter controls than a small internal tool, but one permanent Mac should not handle every untrusted build and production release.
Use narrowly scoped credentials
A compromised credential is less useful when its permissions and lifetime are limited.
Prefer job-specific tokens, temporary credentials, separate keys for different applications, and read-only access where possible. OIDC can be used with supported cloud services and secret managers. Credentials should be exposed only to the protected job that needs them.
Personal access tokens are a poor default for automation. They often have broad permissions, depend on an employee account, and remain valid longer than necessary.
Shared credentials also make rotation harder because revoking one key may affect unrelated projects.
Each credential should have a clear purpose, scope, owner, and rotation process.
Pin the tools used by the build
Locking Swift packages is not enough if the surrounding build tools can change independently.
A reproducible build should also control versions of Xcode, Swift, Fastlane, Ruby gems, CocoaPods, Homebrew tools, code generators, shared CI templates, reusable actions, and supporting Docker images.
References such as main or latest allow build behavior to change without a corresponding change in the application repository.
Critical tools should use a fixed version, immutable revision, or internally managed release. Updates can then pass through the same review process as dependency changes.
Record what went into each release
When a dependency issue appears, the team needs to identify which application releases contain it.
Each production build should be traceable to the application commit, resolved dependency versions, relevant lockfiles, the Xcode version, build tool versions, binary framework versions, CI configuration, and the resulting artifact hash.
For many teams, archiving this information with the release provides enough traceability.
Larger organizations may generate a software bill of materials, but the immediate goal is simpler: determine which released versions used an affected component without reconstructing the build manually.
Prepare for a compromised dependency
Removing a compromised package solves only part of the problem. If its code ran on a developer machine or CI runner, credentials available in that environment may have been exposed.
An incident response process should cover:
- stopping affected pipelines
- identifying machines and runners that executed the code
- listing credentials available in those environments
- rotating credentials that may have been exposed
- reviewing repository and CI audit logs
- checking for unexpected commits, tags, or releases
- cleaning or rebuilding affected runners
- rebuilding the application in a trusted environment
- identifying affected production releases
Rolling back the dependency removes it from subsequent builds. It does not invalidate credentials that may already have been copied.
A practical baseline
An iOS team can improve supply chain security without building a large internal security platform.
Start with these practices:
- Commit dependency lockfiles.
- Review dependency updates separately from feature work.
- Review macros, plugins, executables, scripts, and binaries according to their actual access.
- Keep production secrets out of normal build and test jobs.
- Separate validation and release environments.
- Pin build tools and CI components.
- Record the dependency state of every production release.
- Maintain a procedure for credential rotation and runner cleanup.
Third-party code remains a practical part of iOS development. The goal is not to eliminate it or prove that every dependency is harmless.
The practical goal is to limit what external code can access. If one component is compromised, it should not expose the credentials and infrastructure used to release the application.
