Skip to content

Commit 2144cdc

Browse files
authored
Expose RHEL/Amazon Linux CA roots in chroot mode (#6460)
1 parent 1601d0c commit 2144cdc

8 files changed

Lines changed: 150 additions & 11 deletions

File tree

containers/agent/entrypoint.sh

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -705,9 +705,10 @@ copy_awf_ca_cert() {
705705

706706
copy_system_ca_bundle() {
707707
# Detect and copy the host system CA bundle to a chroot-accessible path.
708-
# On Amazon Linux / RHEL-family systems, the CA bundle lives under /etc/pki/
709-
# which is not mounted into the chroot. This function finds the system bundle
710-
# and copies it to /tmp/awf-lib/ so TLS works regardless of distro.
708+
# On Amazon Linux / RHEL-family systems, the CA bundle often lives under
709+
# /etc/pki/. This function finds the system bundle and, when it is not already
710+
# accessible in the chroot, copies it to /tmp/awf-lib/ so TLS works regardless
711+
# of distro.
711712
#
712713
# In SSL Bump mode, the AWF CA must remain the active trust bundle for MITM
713714
# proxy validation. We only append the system bundle to that staged AWF CA.
@@ -762,11 +763,11 @@ copy_system_ca_bundle() {
762763
fi
763764

764765
# Check if the bundle is already accessible inside the chroot via existing mounts.
765-
# AWF mounts /etc/ssl and /etc/ca-certificates into the chroot; paths under those
766-
# prefixes are already visible. Paths under /etc/pki (RHEL/Amazon Linux) are not.
766+
# AWF mounts the common CA roots under /etc/ssl, /etc/ca-certificates, and the
767+
# RHEL/Amazon Linux CA roots under /etc/pki/ca-trust/extracted and /etc/pki/tls/certs.
767768
local CHROOT_RELATIVE="${SYSTEM_BUNDLE#/host}"
768769
case "$CHROOT_RELATIVE" in
769-
/etc/ssl/*|/etc/ca-certificates/*)
770+
/etc/ssl/*|/etc/ca-certificates/*|/etc/pki/ca-trust/extracted/*|/etc/pki/tls/certs/*)
770771
# Already accessible via existing bind mounts
771772
export SSL_CERT_FILE="$CHROOT_RELATIVE"
772773
export NODE_EXTRA_CA_CERTS="$CHROOT_RELATIVE"
@@ -778,7 +779,7 @@ copy_system_ca_bundle() {
778779
;;
779780
esac
780781

781-
# Bundle is not accessible in chroot (e.g., /etc/pki paths). Copy it.
782+
# Bundle is not accessible in chroot. Copy it.
782783
if mkdir -p /host/tmp/awf-lib 2>/dev/null; then
783784
if cp "$SYSTEM_BUNDLE" /host/tmp/awf-lib/system-ca-certificates.crt 2>/dev/null && \
784785
[ -s /host/tmp/awf-lib/system-ca-certificates.crt ]; then

docs/chroot-mode.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,8 @@ In chroot mode, selective paths are mounted for security instead of the entire f
180180
| `/opt` | `/host/opt:ro` | Tool cache (Python, Node, Go) |
181181
| `/etc/ssl` | `/host/etc/ssl:ro` | SSL certificates |
182182
| `/etc/ca-certificates` | `/host/etc/ca-certificates:ro` | CA certificates |
183+
| `/etc/pki/ca-trust/extracted` | `/host/etc/pki/ca-trust/extracted:ro` | RHEL/Amazon Linux extracted CA bundle roots |
184+
| `/etc/pki/tls/certs` | `/host/etc/pki/tls/certs:ro` | RHEL/Amazon Linux CA certificate directory |
183185
| `/etc/passwd` | `/host/etc/passwd:ro` | User lookup |
184186
| `/etc/group` | `/host/etc/group:ro` | Group lookup |
185187

@@ -191,7 +193,7 @@ When `chroot.binariesSourcePath` is set in stdin config, AWF also mounts:
191193

192194
**Note:** As of v0.13.13, `/proc` is no longer bind-mounted. Instead, a fresh container-scoped procfs is mounted at `/host/proc` during entrypoint initialization. This provides dynamic `/proc/self/exe` resolution required by Java and .NET runtimes.
193195

194-
**System CA Bundle Detection:** The entrypoint automatically detects the host system CA bundle from common locations (Debian/Ubuntu `/etc/ssl/certs/ca-certificates.crt`, RHEL/Amazon Linux `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/pki/tls/cert.pem`, macOS `/etc/ssl/cert.pem`). If the bundle is not already accessible in the chroot via existing mounts (e.g., `/etc/pki` paths), it is copied to `/tmp/awf-lib/system-ca-certificates.crt` and `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, and `GIT_SSL_CAINFO` are set to point at it.
196+
**System CA Bundle Detection:** The entrypoint automatically detects the host system CA bundle from common locations (Debian/Ubuntu `/etc/ssl/certs/ca-certificates.crt`, RHEL/Amazon Linux `/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem`, `/etc/pki/tls/certs/ca-bundle.crt`, `/etc/pki/tls/cert.pem`, macOS `/etc/ssl/cert.pem`). If the bundle is not already accessible in the chroot via the mounted CA paths, it is copied to `/tmp/awf-lib/system-ca-certificates.crt` and `SSL_CERT_FILE`, `NODE_EXTRA_CA_CERTS`, `REQUESTS_CA_BUNDLE`, `CURL_CA_BUNDLE`, and `GIT_SSL_CAINFO` are set to point at it.
195197

196198
### Read-Write Mounts
197199

src/config/mount-policy.test.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,13 @@ describe('mount-policy', () => {
141141

142142
it('exposes the always-mounted /etc allow list', () => {
143143
expect(etcAllowlist()).toEqual(
144-
expect.arrayContaining(['/etc/ssl', '/etc/ca-certificates', '/etc/nsswitch.conf']),
144+
expect.arrayContaining([
145+
'/etc/ssl',
146+
'/etc/ca-certificates',
147+
'/etc/pki/ca-trust/extracted',
148+
'/etc/pki/tls/certs',
149+
'/etc/nsswitch.conf',
150+
]),
145151
);
146152
});
147153
});

src/config/sandbox-mount-policy.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"default": ["/usr", "/bin", "/sbin", "/lib", "/lib64", "/opt", "/sys", "/dev"],
77
"sysroot": ["/sys", "/dev"]
88
},
9-
"etc": ["/etc/ssl", "/etc/ca-certificates", "/etc/alternatives", "/etc/ld.so.cache", "/etc/nsswitch.conf"]
9+
"etc": ["/etc/ssl", "/etc/ca-certificates", "/etc/pki/ca-trust/extracted", "/etc/pki/tls/certs", "/etc/alternatives", "/etc/ld.so.cache", "/etc/nsswitch.conf"]
1010
},
1111
"home": {
1212
"$comment": "Agent $HOME exposure. `toolSubdirs` is the ALLOW list: tool caches, language toolchains and agent state the agent legitimately needs. `forbiddenSubdirs` is a DENY guard: dirs whose primary purpose is storing credentials and which must NEVER be added to the allow list. Compose mounts an empty home + binds toolSubdirs on top; sbx mounts toolSubdirs wholesale instead of the whole $HOME.",

src/services/agent-volumes-basic.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as fs from 'fs';
12
import { generateDockerCompose, mockNetworkConfig, useAgentVolumesTestConfig } from './service-test-setup.test-utils';
23

34
// Create mock functions (must remain per-file — jest.mock() is hoisted before imports)
@@ -163,6 +164,16 @@ describe('agent service', () => {
163164
// Should include /etc subdirectories (read-only)
164165
expect(volumes).toContain('/etc/ssl:/host/etc/ssl:ro');
165166
expect(volumes).toContain('/etc/ca-certificates:/host/etc/ca-certificates:ro');
167+
if (fs.existsSync('/etc/pki/ca-trust/extracted')) {
168+
expect(volumes).toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
169+
} else {
170+
expect(volumes).not.toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
171+
}
172+
if (fs.existsSync('/etc/pki/tls/certs')) {
173+
expect(volumes).toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
174+
} else {
175+
expect(volumes).not.toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
176+
}
166177
expect(volumes).toContain('/etc/alternatives:/host/etc/alternatives:ro');
167178
expect(volumes).toContain('/etc/ld.so.cache:/host/etc/ld.so.cache:ro');
168179
// /etc/hosts is always a custom hosts file in a secure chroot temp dir (for pre-resolved domains)

src/services/agent-volumes/etc-mounts.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ function createMinimalConfig(overrides: Partial<WrapperConfig> = {}): WrapperCon
1616
}
1717

1818
describe('buildEtcMounts', () => {
19+
afterEach(() => {
20+
jest.restoreAllMocks();
21+
});
22+
1923
describe('sysroot gating by runnerTopology', () => {
2024
it('returns empty array when runnerTopology is arc-dind (sysroot provides /etc)', () => {
2125
const config = createMinimalConfig({ runnerTopology: 'arc-dind' });
@@ -51,8 +55,42 @@ describe('buildEtcMounts', () => {
5155
const mounts = buildEtcMounts(config);
5256
expect(mounts).toContain('/etc/ssl:/host/etc/ssl:ro');
5357
expect(mounts).toContain('/etc/ca-certificates:/host/etc/ca-certificates:ro');
58+
if (fs.existsSync('/etc/pki/ca-trust/extracted')) {
59+
expect(mounts).toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
60+
} else {
61+
expect(mounts).not.toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
62+
}
63+
if (fs.existsSync('/etc/pki/tls/certs')) {
64+
expect(mounts).toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
65+
} else {
66+
expect(mounts).not.toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
67+
}
5468
expect(mounts).toContain('/etc/nsswitch.conf:/host/etc/nsswitch.conf:ro');
5569
});
70+
71+
it('omits optional RHEL CA mounts when host sources are unavailable', () => {
72+
if (fs.existsSync('/etc/pki/ca-trust/extracted') || fs.existsSync('/etc/pki/tls/certs')) {
73+
return;
74+
}
75+
const config = createMinimalConfig({ dockerHostPathPrefix: undefined });
76+
const mounts = buildEtcMounts(config);
77+
expect(mounts).not.toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
78+
expect(mounts).not.toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
79+
});
80+
81+
it('keeps optional RHEL CA mounts when split-fs prefixed sources exist', () => {
82+
const prefixRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'awf-prefix-'));
83+
try {
84+
fs.mkdirSync(path.join(prefixRoot, 'etc/pki/ca-trust/extracted'), { recursive: true });
85+
fs.mkdirSync(path.join(prefixRoot, 'etc/pki/tls/certs'), { recursive: true });
86+
const config = createMinimalConfig({ dockerHostPathPrefix: prefixRoot });
87+
const mounts = buildEtcMounts(config);
88+
expect(mounts).toContain('/etc/pki/ca-trust/extracted:/host/etc/pki/ca-trust/extracted:ro');
89+
expect(mounts).toContain('/etc/pki/tls/certs:/host/etc/pki/tls/certs:ro');
90+
} finally {
91+
fs.rmSync(prefixRoot, { recursive: true, force: true });
92+
}
93+
});
5694
});
5795

5896
describe('DinD mode with dockerHostPathPrefix', () => {

src/services/agent-volumes/etc-mounts.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ import { getSafeHostUid, getSafeHostGid } from '../../host-identity';
66
import { isSysrootEnabled } from '../sysroot-service';
77
import { etcAllowlist } from '../../config/mount-policy';
88

9+
const OPTIONAL_ETC_CA_MOUNTS = new Set<string>([
10+
'/etc/pki/ca-trust/extracted',
11+
'/etc/pki/tls/certs',
12+
]);
13+
14+
function normalizeDockerHostPathPrefix(prefix: string): string {
15+
const trimmed = prefix.trim();
16+
if (!trimmed) return '';
17+
const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
18+
return withLeadingSlash.replace(/\/+$/, '') || '/';
19+
}
20+
21+
function bindMountSourceExists(hostPath: string, config: WrapperConfig): boolean {
22+
if (fs.existsSync(hostPath)) {
23+
return true;
24+
}
25+
26+
if (!config.dockerHostPathPrefix) {
27+
return false;
28+
}
29+
30+
const normalizedPrefix = normalizeDockerHostPathPrefix(config.dockerHostPathPrefix);
31+
if (!normalizedPrefix || normalizedPrefix === '/') {
32+
return false;
33+
}
34+
35+
return fs.existsSync(`${normalizedPrefix}${hostPath}`);
36+
}
37+
938
/**
1039
* Synthesize a minimal /etc/passwd or /etc/group file in the staging directory.
1140
* Used when the runner doesn't have these files (e.g., minimal ARC-DinD containers).
@@ -67,7 +96,9 @@ export function buildEtcMounts(config: WrapperConfig): string[] {
6796
return [];
6897
}
6998

70-
const mounts: string[] = etcAllowlist().map((p) => `${p}:/host${p}:ro`);
99+
const mounts: string[] = etcAllowlist()
100+
.filter((p) => !OPTIONAL_ETC_CA_MOUNTS.has(p) || bindMountSourceExists(p, config))
101+
.map((p) => `${p}:/host${p}:ro`);
71102

72103
if (!shouldUseDockerHostStaging(config.dockerHostPathPrefix)) {
73104
mounts.push('/etc/passwd:/host/etc/passwd:ro');

tests/entrypoint-phase-functions.test.sh

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,56 @@ else
149149
fail "copy_system_ca_bundle() does not safely append system roots to the staged AWF CA bundle"
150150
fi
151151

152+
if printf '%s\n' "${COPY_SYSTEM_CA_BUNDLE_BLOCK}" | grep -Fq '/etc/pki/ca-trust/extracted/*|/etc/pki/tls/certs/*)'; then
153+
pass "copy_system_ca_bundle() treats mounted RHEL/Amazon Linux CA paths as chroot-accessible"
154+
else
155+
fail "copy_system_ca_bundle() does not recognize mounted RHEL/Amazon Linux CA paths as chroot-accessible"
156+
fi
157+
158+
run_copy_system_ca_bundle_fixture() {
159+
local tmp_dir
160+
tmp_dir="$(mktemp -d)"
161+
local host_root="${tmp_dir}/host-root"
162+
local fixture_entrypoint="${tmp_dir}/entrypoint-fixture.sh"
163+
mkdir -p "${host_root}/etc/pki/tls/certs" "${host_root}/etc/ssl/certs"
164+
printf '%s\n' "fixture-ca-cert" > "${host_root}/etc/pki/tls/certs/ca-bundle.crt"
165+
166+
awk '$0 != "main \"$@\""' "${ENTRYPOINT}" > "${fixture_entrypoint}"
167+
sed -i "s#/host#\${AWF_TEST_HOST_ROOT}#g" "${fixture_entrypoint}"
168+
169+
(
170+
set -e
171+
# shellcheck disable=SC1090
172+
. "${fixture_entrypoint}"
173+
174+
AWF_SSL_BUMP_ENABLED="false"
175+
AWF_CA_CHROOT=""
176+
AWF_TEST_HOST_ROOT="${host_root}"
177+
178+
unset SSL_CERT_FILE NODE_EXTRA_CA_CERTS REQUESTS_CA_BUNDLE CURL_CA_BUNDLE GIT_SSL_CAINFO SYSTEM_CA_CHROOT
179+
copy_system_ca_bundle
180+
[ "${SSL_CERT_FILE}" = "/etc/pki/tls/certs/ca-bundle.crt" ]
181+
[ -r "${AWF_TEST_HOST_ROOT}${SSL_CERT_FILE}" ]
182+
[ "${NODE_EXTRA_CA_CERTS}" = "${SSL_CERT_FILE}" ]
183+
184+
ln -sf ../../pki/tls/certs/ca-bundle.crt "${host_root}/etc/ssl/certs/ca-certificates.crt"
185+
unset SSL_CERT_FILE NODE_EXTRA_CA_CERTS REQUESTS_CA_BUNDLE CURL_CA_BUNDLE GIT_SSL_CAINFO SYSTEM_CA_CHROOT
186+
copy_system_ca_bundle
187+
[ "${SSL_CERT_FILE}" = "/etc/ssl/certs/ca-certificates.crt" ]
188+
[ -r "${AWF_TEST_HOST_ROOT}${SSL_CERT_FILE}" ]
189+
[ "${NODE_EXTRA_CA_CERTS}" = "${SSL_CERT_FILE}" ]
190+
)
191+
local result=$?
192+
rm -rf "${tmp_dir}"
193+
return "${result}"
194+
}
195+
196+
if run_copy_system_ca_bundle_fixture; then
197+
pass "copy_system_ca_bundle() exports chroot-readable CA paths for direct RHEL bundles and /etc/ssl symlink targets"
198+
else
199+
fail "copy_system_ca_bundle() does not export chroot-readable CA paths for RHEL bundle fixtures"
200+
fi
201+
152202
if grep -Eq '\[ -n "\$\{SYSTEM_CA_CHROOT\}" \]' "${ENTRYPOINT}"; then
153203
pass "run_chroot_command() cleans up copied system CA bundles"
154204
else

0 commit comments

Comments
 (0)