When developers connect to a cloud Mac for the first time, the terminal typically asks whether to accept an unknown host. Typing yes is convenient for an interactive session, and setting StrictHostKeyChecking=no in CI is even easier, but both approaches bypass identity verification. If DNS, address configuration, or network routing points to the wrong destination, credentials and build artifacts may be sent to an unintended machine. The correct approach is to obtain the host key fingerprint through a trusted channel first, then configure developer workstations and automated jobs to accept only that key.
Establish an Independent Trust Baseline First
A host key proves that “this connection is reaching the expected Mac.” It is separate from the private key used for user authentication. For an Ed25519 host key, open a local terminal on the target machine through the Web VNC provided in the XcodeVM console, then read its public-key fingerprint:
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256
Record the complete SHA256: value along with the instance identifier, connection address, and method used to retrieve it. Do not obtain this value solely through the SSH connection being verified, as that would make the verification circular.
Next, retrieve the public key currently presented by the remote host from the developer workstation:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > ~/.ssh/xcodevm_known_hosts.new
ssh-keygen -lf ~/.ssh/xcodevm_known_hosts.new -E sha256
Compare the two fingerprints character by character. Install the file only after they match:
install -m 600 ~/.ssh/xcodevm_known_hosts.new ~/.ssh/xcodevm_known_hosts
rm -f ~/.ssh/xcodevm_known_hosts.new
ssh-keyscanretrieves a public key; it does not prove that the key is trustworthy. Without an independently obtained fingerprint as a baseline, its output must not be added directly to a production trust file.
Isolate known_hosts by Project
The global ~/.ssh/known_hosts file works well for routine interactive use, but team projects should use a dedicated file. This makes changes explicit and reviewable, while preventing automated jobs from being affected when an individual cleans up personal host records.
Define an alias in ~/.ssh/config:
Host xcodevm-build
HostName 192.0.2.10
User builder
IdentityFile ~/.ssh/xcodevm_build_ed25519
IdentitiesOnly yes
UserKnownHostsFile ~/.ssh/xcodevm_known_hosts
StrictHostKeyChecking yes
HashKnownHosts yes
ServerAliveInterval 30
ServerAliveCountMax 3
IdentitiesOnly yes prevents the client from trying too many identity files in sequence. StrictHostKeyChecking yes ensures that an unknown or changed key causes an immediate failure. Before connecting, inspect the resolved configuration to make sure no other rule overrides the alias:
ssh -G xcodevm-build |
grep -E '^(hostname|user|identityfile|userknownhostsfile|stricthostkeychecking) '
If a project uses multiple machines, keep a separate entry for each one and include its instance identifier in the internal change record. Do not use a broad alias that points to an address that may change unpredictably.
Pin the Fingerprint in CI
CI should not permanently inherit a developer’s personal trust file. A safer approach is to store the expected SHA256 fingerprint as a protected variable, then scan and compare the key before creating a temporary known_hosts file for each job.
set -euo pipefail
: "${MAC_HOST:?MAC_HOST is required}"
: "${EXPECTED_ED25519_SHA256:?fingerprint is required}"
scan_file="$(mktemp)"
known_hosts_file="$(mktemp)"
trap 'rm -f "$scan_file" "$known_hosts_file"' EXIT
ssh-keyscan -T 5 -t ed25519 "$MAC_HOST" > "$scan_file" 2>/dev/null
actual="$(ssh-keygen -lf "$scan_file" -E sha256 | awk '{print $2}')"
if [ "$actual" != "$EXPECTED_ED25519_SHA256" ]; then
printf '%s
' "SSH host key verification failed" >&2
exit 1
fi
install -m 600 "$scan_file" "$known_hosts_file"
ssh \
-o UserKnownHostsFile="$known_hosts_file" \
-o StrictHostKeyChecking=yes \
-o IdentitiesOnly=yes \
"$MAC_USER@$MAC_HOST" \
'uname -m && sw_vers -productVersion'
Manage the private key, expected fingerprint, and host address separately. Logs may include the observed fingerprint to aid troubleshooting, but they must not expose private-key contents, connection passwords, or unredacted environment variables.
Handle Host Key Changes Correctly
A host key may change because the system was reset, the host keys were regenerated, or a connection address was reused. It may also indicate that the connection has been redirected to the wrong destination. When REMOTE HOST IDENTIFICATION HAS CHANGED appears, stop the automated job first. Do not simply delete the existing record and retry.
Perform a safe rotation in this order:
- In the console, confirm that the instance identifier, connection address, and recent operations are all as expected.
- Use the local terminal available through Web VNC to read the Ed25519 public-key fingerprint again.
- Have another maintainer review both the old and new fingerprints and the reason for the change.
- Scan the remote public key, verify the new fingerprint, and generate a new trust file.
- Update the test job first, then complete one read-only connection and environment check.
- Update the production job, then revoke the old fingerprint.
For hashed records, locate the old entry first:
ssh-keygen -F "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts
ssh-keygen -R "$MAC_HOST" -f ~/.ssh/xcodevm_known_hosts
Run the second command only after the new fingerprint has been independently verified. Removing the old record is part of rotation; it is not a verification step.
Complete Acceptance with a Checklist
Before going live, verify at least the following:
| Check | Acceptance criterion |
|---|---|
| Fingerprint source | Read from a local terminal on the target machine |
| Key algorithm | Explicitly pin Ed25519; do not accept arbitrary algorithms |
| File permissions | .ssh is 700 and the trust file is 600 |
| Client policy | StrictHostKeyChecking=yes |
| CI isolation | Each job uses a separate temporary file |
| Change handling | Stop the job, verify the change, then replace the key |
| Log contents | Retain the fingerprint and failure stage without exposing credentials |
Also run a negative test: temporarily replace the expected fingerprint with an incorrect value and confirm that the job exits before executing any remote command. Restore the correct value, then verify a read-only command, repository access, and the build entry point. This proves that the safeguard is actively enforced rather than merely present in a configuration file.
An SSH trust chain does not require a complex system. The key is to replace ad hoc approval during the first connection and after key changes with a reviewable process. A trusted terminal provides the baseline, a project-specific file stores the pinned relationship, CI verifies it before every connection, and any change triggers a pause for validation. Once these steps are in place, remote development and automated builds can share the same explicit host identity boundary.
Frequently asked questions
Can a host key returned by ssh-keyscan be trusted immediately?
No. ssh-keyscan only reads the key currently presented by the remote host. Its SHA256 fingerprint must match a value obtained through an independent, trusted path to the Mac.
Can I delete the known_hosts entry after a warning and reconnect?
No. First verify the destination address and system state, then retrieve the new fingerprint through a trusted terminal. Replace the old entry only after the fingerprints match.
Should CI reuse a developer’s personal known_hosts file?
No. Each job should create a dedicated file with mode 600, enable StrictHostKeyChecking explicitly, and remove the temporary trust file when the job finishes.
XcodeVM macOS Cloud Hosts
Choose a dedicated physical machine suited to your current workload
Review the memory, storage, node, and billing cycle before proceeding to checkout. All nodes run continuously year-round; actual availability is based on the real-time status returned by the console.