A previously stable CI pipeline may suddenly begin connecting to unfamiliar destinations. Do not block them immediately. Build tools, dependency resolvers, test processes, and background update tasks can all initiate outbound connections. An effective investigation starts with a reproducible build and a defined observation window. Record which processes run, where they connect, and when each connection occurs, then organize the findings into a baseline.
Define a comparable sampling window
Choose a time when dependencies are already installed, the workspace is clean, and no other team jobs are running. Record the commit hash, build command, macOS version, Xcode version, and start time. Run only one job during the first capture so that processes shared by multiple pipelines do not become impossible to attribute.
Prepare two datasets: one from a minimal build and another from the complete test suite. The first reveals connections related to dependency resolution and compilation, while the second also covers simulators, test services, and artifact uploads. Run each dataset twice. The second run usually distinguishes first-time downloads from traffic generated by a steady-state build.
An outbound baseline is not a permanently valid list of IP addresses. It is evidence explaining why a known job accesses a particular type of service during a known stage.
Observe connections through three layers of evidence
Processes and active connections
lsof helps identify which processes still hold connections at the moment of capture. Using numeric addresses avoids generating additional DNS traffic through reverse lookups.
sudo lsof -nP -iTCP -sTCP:ESTABLISHED \
| awk 'NR == 1 || $1 ~ /xcode|swift|git|ruby|node|curl/'
Do not filter only by process name and discard the original output. Save the complete snapshot first, then generate a more readable subset. Build scripts may download files through system utilities whose names contain no project-specific keywords.
Traffic and short-lived connections
nettop provides a real-time, per-process summary, while tcpdump can preserve TCP handshakes that end within tens of milliseconds. When working remotely, capture only outbound SYN packets and exclude payloads to reduce the amount of application data written to the logs.
nettop -P -L 1
sudo tcpdump -i any -nn -l \
'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0'
Before starting the capture, confirm the remote address of the current SSH session. An incorrect filter can produce a large amount of noise, but these observation commands do not modify firewall rules.
Create an archivable capture bundle
The following script saves system information, routing details, connection snapshots, and a 60-second record of outbound handshakes. Before running it, prepare the build so that it is ready to start. Once the script begins, launch the build from another session.
#!/bin/zsh
set -euo pipefail
OUT="${1:-$HOME/ci-egress-$(date -u +%Y%m%dT%H%M%SZ)}"
mkdir -p "$OUT"
date -u > "$OUT/time.txt"
sw_vers > "$OUT/system.txt"
xcodebuild -version > "$OUT/xcode.txt"
route -n get default > "$OUT/default-route.txt"
scutil --dns > "$OUT/dns.txt"
sudo lsof -nP -i > "$OUT/lsof-before.txt"
sudo tcpdump -i any -nn \
'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' \
-w "$OUT/outbound-syn.pcap" &
CAPTURE_PID=$!
sleep 60
sudo kill -INT "$CAPTURE_PID"
wait "$CAPTURE_PID" || true
sudo lsof -nP -i > "$OUT/lsof-after.txt"
nettop -P -L 1 > "$OUT/nettop.txt"
printf '%s
' "$OUT"
If the build takes longer than 60 seconds, capture the dependency resolution, compilation, testing, and archiving stages separately instead of extending one capture indefinitely. Stage-specific results make it easier to determine which step introduced a new connection.
Turn destinations into a communication baseline
Do not treat an unfamiliar IP address as suspicious by default. Classify it using the process, port, build stage, and project configuration. Content delivery networks may change addresses frequently, so the baseline should focus on purpose and verifiable domain names. Retain IP addresses only as evidence from that specific capture.
For an unknown process, run ps -o pid,ppid,user,start,command -p PID and then inspect its parent process. If a connection appears only once, convert both the packet-capture timestamps and CI log timestamps to UTC and align them instead of relying on memory.
Common misclassifications and safety boundaries
The most common mistake is treating a dynamic IP address as a stable identity. Another is collecting lsof output only after the build has finished, when short-lived connections have already disappeared. A third is enabling deny rules directly on a remote node. Rules that have not been validated through a local console may cut off SSH, DNS, or dependency downloads at the same time.
A safer sequence is to observe, classify, reproduce, and review before applying restrictions. If outbound access must be tightened, preserve the existing management connection and DNS path first, prepare rollback commands, and validate the changes on a single non-critical job. Firewall rules, proxy configuration, and build scripts must be version-controlled rather than stored only on the node.
Integrate the baseline into routine reviews
Repeat the same capture whenever the toolchain, dependency sources, or upload workflow changes. Compare new process–destination–stage combinations rather than the number of IP address entries. Every allowed connection should identify its owner, purpose, and review conditions. An unexplained connection must not receive permanent approval through a note alone.
Retain the capture script, raw files, classification table, and corresponding commit hash. If the team later encounters an unexpected download, a slower build, or suspected credential exfiltration, it can begin with actual differences instead of guessing what normal traffic should look like.
Frequently asked questions
Should captured destination IPs be copied directly into a firewall allowlist?
No. Dependency and hosting services often use dynamic or shared addresses. Classify destinations by purpose and domain before choosing an enforcement method.
Why do lsof and tcpdump report different connection counts?
lsof shows connections that remain open at the instant of collection, while tcpdump also records short connection attempts that ended during the capture window.
Run builds on dedicated Apple Silicon physical nodes
Configure nodes by model, region, and rental period. Actual availability is confirmed in real time by the control panel.