Engineering Dossier

How to Keep Secrets out of Cloud Mac CI Build Logs

How to Keep Secrets out of Cloud Mac CI Build Logs

After a pipeline fails, teams typically upload the complete logs, test attachments, and result bundles to shared storage so another engineer can take over the investigation. The real danger is not the conspicuous TOKEN= line, but command arguments echoed in debug mode, request headers, temporary signing paths, and environment snapshots attached by test code. When a cloud Mac handles multiple jobs over an extended period, this data may also end up in caches and archives. Log redaction must therefore happen at three points: the output source, the pipeline, and the upload gate.

Map the log data flow first

Do not start by writing one regular expression that attempts to cover all text. First identify where information enters, which processes it passes through, and where it is ultimately stored.

Entry point Common leak Priority action
Shell set -x echoes expanded variables Disable tracing before sensitive steps
Command arguments Keys and tokens appear directly in process arguments Use a restricted temporary file or standard input
Environment variables Diagnostic scripts print the complete environment Output only an allowlist
Tests Request headers and account data are written to attachments Sanitize centrally in the test helper layer
Result bundles Screenshots, activity logs, and diagnostic files are uploaded together Scan after export and before publication

Redaction cannot make an already exposed secret safe again. Once you confirm that a real value entered a shareable log, stop publishing that artifact and rotate the secret according to its type.

On the OakVM node, create an isolated directory for each job and make it readable and writable only by the current user. Keep logs, result bundles, and files awaiting upload in separate locations so scanned files are not mixed with their originals.

umask 077
run_id="$(date -u +%Y%m%dT%H%M%SZ)-$$"
root="$HOME/ci-runs/$run_id"
mkdir -p "$root/raw" "$root/safe" "$root/results"

Close leak paths at the output source

Keep secrets out of command arguments

Process arguments may be read by diagnostic tools, crash reports, or scripts launched by the same user. When calling an internal upload script, do not pass --token "$TOKEN". A safer approach is to have the script read a temporary file with 600 permissions and delete it immediately afterward. If the tool supports standard input, the secret can also be passed that way.

Shell tracing should be disabled by default. If you genuinely need to inspect ordinary commands, enable it only for a short section that does not touch sensitive variables, and run set +x before loading credentials. Never print the complete environment directly; replace that output with an allowlist:

printf 'PATH=%s
' "$PATH"
printf 'DEVELOPER_DIR=%s
' "$DEVELOPER_DIR"
xcodebuild -version
sw_vers

Treat paths as sensitive data as well. Usernames, project codenames, and temporary signing directories can reveal team structure. Before publishing a log, replace the workspace root with $WORKSPACE, but retain filenames and line numbers so the log remains useful for troubleshooting.

Build a redaction pipeline that preserves failures

Redaction should primarily match actual secret values rather than merely searching for field names such as password or secret. Keyword matching produces false positives and cannot catch unlabeled tokens. The filter below reads values to mask from a restricted file and replaces them from longest to shortest so a shorter value cannot disrupt a longer match.

import os
import sys
from pathlib import Path

secret_file = Path(os.environ["REDACT_FILE"])
values = [
    line.rstrip("
")
    for line in secret_file.read_text(encoding="utf-8").splitlines()
    if line.strip()
]
values.sort(key=len, reverse=True)

for line in sys.stdin:
    for value in values:
        line = line.replace(value, "[REDACTED]")
    sys.stdout.write(line)

When running the build, preserve the actual xcodebuild exit code. If you inspect only the trailing tee process, a failed build may be incorrectly reported as successful.

#!/bin/bash
set -o pipefail

set +x
REDACT_FILE="$root/secrets.txt" \
xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -resultBundlePath "$root/results/App.xcresult" \
  build 2>&1 |
python3 ci/redact.py |
tee "$root/safe/build.log"

statuses=("${PIPESTATUS[@]}")
exit "${statuses[0]}"

secrets.txt must have 600 permissions and be deleted by the job’s exit hook. Multiline private keys are not suitable for line-by-line replacement. The correct approach is to prevent them from being printed at the source and add only stable identifiers that may appear in text to the masking file.

Scan result bundles and test attachments

Passing the text-log checks does not mean the entire job is safe to upload. Result bundles may contain test screenshots, failure attachments, activity logs, and diagnostic information. First copy them into an isolated directory, export only the content that genuinely needs to be shared, and then scan the export directory. Do not publish complete result bundles by default.

Use two rule classes to reduce false positives

The first rule class matches actual secret values from the current job and blocks publication immediately on a match. The second checks for high-risk structures such as authorization headers, private-key boundaries, credential-bearing URLs, and suspicious long tokens. Matches from structural rules should be reviewed manually. Do not automatically classify every occurrence of token as a leak, because source filenames and test descriptions may also contain that word.

Scan reports should record only the file path, line number, and rule ID. Do not copy the matched content into the report and expose it again. For binary attachments, identify the file type first. Files that cannot be parsed safely should be excluded from shared artifacts by default rather than extracted with strings and published directly.

Make redaction an upload gate

A maintainable gate includes at least four states: the build exit code, exact-value scanning, structural-rule review, and the artifact allowlist. The upload step must not start until every state is complete.

At the end of each job, check that:

  • the raw directory remains accessible only to the current user;
  • the logs contain no remaining workspace root, authorization headers, or actual secret values;
  • the result bundle contains no irrelevant screenshots, network responses, or environment snapshots;
  • the upload manifest contains only redacted logs, required reports, and explicitly selected attachments;
  • the cleanup script removes secret files without deleting safe copies still needed for investigation.

Finally, run a negative test. Inject a dedicated fake token into a temporary job and send it through standard output, standard error, and a test attachment to confirm that the gate blocks publication. Then force the build to return a nonzero status and verify that the redaction pipeline does not turn the failure into a success. The log security controls are truly part of the engineering workflow only when both types of tests pass.

Frequently asked questions

Is redacting logs immediately before upload sufficient?

No. A secret may already exist in terminal output, caches, or result bundles. Disable shell tracing and keep secrets out of command arguments before adding redaction as a secondary control.

How do I preserve the real xcodebuild exit status through a redaction pipeline?

Enable pipefail in Bash and capture PIPESTATUS[0] immediately after the pipeline. Do not use the status returned by tee or the redaction process as the build result.

Which artifacts should be checked in addition to plain-text logs?

Inspect result bundles, test attachments, diagnostic archives, copied export configuration files, and reports generated by custom scripts.

OAKVM BUILD NODE

Run your next build on a dedicated physical node

Choose OakVM M4 or OakVM M4 Pro and rent a cloud Mac by the day, week, month, or quarter. Node availability is reported in real time by the control panel.

Choose a configuration and rent now