1pub const AGENT_PATH: &str = "/usr/local/bin/alien-sandbox-agent";
17
18pub const AGENT_PORT: u16 = 8971;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Isolation {
32 UidSplit,
35 Platform,
38}
39
40impl Isolation {
41 pub fn env_value(self) -> &'static str {
43 match self {
44 Self::UidSplit => "uid-split",
45 Self::Platform => "platform",
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum Authorization {
53 Transport,
56 Capability,
58}
59
60impl Authorization {
61 pub fn env_value(self) -> &'static str {
63 match self {
64 Self::Transport => "transport",
65 Self::Capability => "capability",
66 }
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct SandboxImage {
73 pub exec_uid: u32,
75 pub session_root: &'static str,
77 pub port: u16,
79 pub authorization: Authorization,
80 pub isolation: Isolation,
81}
82
83pub const AWS_MICROVM: SandboxImage = SandboxImage {
89 exec_uid: 60000,
90 session_root: "/sandbox",
91 port: AGENT_PORT,
92 authorization: Authorization::Transport,
93 isolation: Isolation::UidSplit,
94};
95
96pub const GCP_AGENT_PLATFORM: SandboxImage = SandboxImage {
107 exec_uid: 1000,
108 session_root: "/sandbox",
109 port: 8080,
110 authorization: Authorization::Transport,
111 isolation: Isolation::Platform,
112};
113
114pub fn identity_setup(image: &SandboxImage) -> String {
116 let SandboxImage {
117 exec_uid,
118 session_root,
119 ..
120 } = *image;
121 format!(
122 r#"RUN printf 'sandbox:x:{exec_uid}:{exec_uid}::{session_root}:/sbin/nologin\n' >> /etc/passwd \
123 && printf 'sandbox:x:{exec_uid}:\n' >> /etc/group \
124 && mkdir -p {session_root} \
125 && chown {exec_uid}:{exec_uid} {session_root} \
126 && chmod 0700 {session_root}"#
127 )
128}
129
130pub fn contract_env(image: &SandboxImage) -> String {
132 let SandboxImage {
133 exec_uid,
134 session_root,
135 port,
136 authorization,
137 isolation,
138 } = *image;
139 format!(
140 r#"ENV ALIEN_SANDBOX_ROOT={session_root} \
141 ALIEN_SANDBOX_PORT={port} \
142 ALIEN_SANDBOX_AUTHORIZATION={authorization} \
143 ALIEN_SANDBOX_EXEC_UID={exec_uid} \
144 ALIEN_SANDBOX_EXEC_GID={exec_uid} \
145 ALIEN_SANDBOX_ISOLATION={isolation}"#,
146 authorization = authorization.env_value(),
147 isolation = isolation.env_value(),
148 )
149}
150
151pub fn entrypoint(image: &SandboxImage) -> String {
153 let ending = match image.isolation {
154 Isolation::UidSplit => String::new(),
155 Isolation::Platform => format!(
156 "# Explicit gid so a runtime that does not read /etc/passwd cannot start the agent in \
157 group 0, which\n# makes the exec drop a privilege crossing whose setgroups needs a \
158 CAP_SETGID this image lacks, so\n# every exec fails.\nUSER {uid}:{uid}\n",
159 uid = image.exec_uid
160 ),
161 };
162 format!(
163 "EXPOSE {port}\n{ending}ENTRYPOINT [\"{AGENT_PATH}\"]",
164 port = image.port
165 )
166}
167
168#[cfg(test)]
170const GCP_DOCKERFILE: &str = "docker/Dockerfile.alien-sandbox-agent";
171
172#[cfg(test)]
174const GCP_DOCKERFILE_UPDATE: &str = "UPDATE_SANDBOX_AGENT_DOCKERFILE";
175
176#[cfg(test)]
184fn gcp_agent_platform_dockerfile() -> String {
185 let image = &GCP_AGENT_PLATFORM;
186 format!(
187 r#"# Generated by `cargo test -p alien-core --lib sandbox_image`. Do not edit by hand.
188# Regenerate with {GCP_DOCKERFILE_UPDATE}=1 in front of that command.
189#
190# Multi-arch build for the alien-sandbox-agent Docker image
191# Run directly as the GCP Agent Platform sandbox; nothing layers on top of it
192
193FROM docker.io/chainguard/wolfi-base:latest AS binary-selector
194
195COPY target/aarch64-unknown-linux-musl/release/alien-sandbox-agent /tmp/alien-sandbox-agent-aarch64
196COPY target/x86_64-unknown-linux-musl/release/alien-sandbox-agent /tmp/alien-sandbox-agent-x86_64
197
198ARG TARGETARCH
199RUN case "$TARGETARCH" in \
200 amd64) cp /tmp/alien-sandbox-agent-x86_64 /tmp/alien-sandbox-agent ;; \
201 arm64) cp /tmp/alien-sandbox-agent-aarch64 /tmp/alien-sandbox-agent ;; \
202 *) echo "unsupported TARGETARCH '$TARGETARCH'" >&2; exit 1 ;; \
203 esac
204
205FROM docker.io/chainguard/wolfi-base:latest
206
207# git is for the sandboxed command, not the agent, and pulls 24 transitive packages. That cost
208# lands here because this image is the sandbox, with no customer base image underneath to carry it.
209RUN apk add --no-cache git
210
211# Root-owned and unwritable by uid {exec_uid}: the supervised command runs under that uid and must not
212# be able to rewrite its own supervisor.
213COPY --from=binary-selector --chown=0:0 --chmod=0755 \
214 /tmp/alien-sandbox-agent {AGENT_PATH}
215
216# Numeric ids and a plain append rather than adduser, which differs across base distributions.
217# Linux runs a process under a uid with no passwd entry, but tooling inside the sandbox reads one.
218{identity}
219
220# The template carries no env, so the contract lives here, and none of it is optional. transport
221# serves an uncapabilitied request only from a socket `peer::transport_may_serve` cannot trace
222# back to the exec uid, and the agent refuses the mode where /proc/net/tcp is unreadable.
223{env}
224
225# The release build resolves tracing-subscriber once across every package it names, and five of
226# them ask for env-filter, so the agent's fmt::init() has an EnvFilter under it. Unset, that
227# filter discards the startup warning saying this image serves requests without a capability.
228ENV RUST_LOG=info
229
230{entrypoint}
231"#,
232 exec_uid = image.exec_uid,
233 identity = identity_setup(image),
234 env = contract_env(image),
235 entrypoint = entrypoint(image),
236 )
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 fn committed_path() -> std::path::PathBuf {
244 std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
245 .join("../..")
246 .join(GCP_DOCKERFILE)
247 }
248
249 fn first_difference(committed: &str, rendered: &str) -> String {
252 for (index, (left, right)) in committed.lines().zip(rendered.lines()).enumerate() {
253 if left != right {
254 let line = index + 1;
255 return format!("line {line}: committed {left:?}, contract renders {right:?}");
256 }
257 }
258 format!(
259 "committed has {} lines, the contract renders {}",
260 committed.lines().count(),
261 rendered.lines().count()
262 )
263 }
264
265 #[test]
268 fn the_committed_gcp_dockerfile_is_what_the_contract_renders() {
269 let path = committed_path();
270 let rendered = gcp_agent_platform_dockerfile();
271
272 if std::env::var_os(GCP_DOCKERFILE_UPDATE).is_some() {
273 std::fs::write(&path, &rendered)
274 .unwrap_or_else(|error| panic!("{} must be writable: {error}", path.display()));
275 return;
276 }
277
278 let committed = std::fs::read_to_string(&path)
279 .unwrap_or_else(|error| panic!("{} must be readable: {error}", path.display()));
280 assert!(
281 committed == rendered,
282 "{GCP_DOCKERFILE} has drifted from the contract it is rendered from.\n\
283 {}\n\
284 Regenerate it: {GCP_DOCKERFILE_UPDATE}=1 cargo test -p alien-core --lib sandbox_image",
285 first_difference(&committed, &rendered)
286 );
287 }
288
289 #[test]
295 fn the_two_images_carry_the_identities_their_stacks_were_built_against() {
296 assert_eq!(AWS_MICROVM.port, 8971);
297 assert_eq!(AWS_MICROVM.exec_uid, 60000);
298 assert_eq!(AWS_MICROVM.session_root, "/sandbox");
299 assert_eq!(GCP_AGENT_PLATFORM.port, 8080);
300 assert_eq!(GCP_AGENT_PLATFORM.exec_uid, 1000);
301 assert_eq!(GCP_AGENT_PLATFORM.session_root, "/sandbox");
302 for image in [&AWS_MICROVM, &GCP_AGENT_PLATFORM] {
303 assert_ne!(image.exec_uid, 0, "the exec uid must never be root");
304 }
305 }
306
307 #[test]
309 fn the_ending_an_image_declares_follows_its_isolation() {
310 assert!(!entrypoint(&AWS_MICROVM).contains("USER "));
311 assert!(contract_env(&AWS_MICROVM).contains("ALIEN_SANDBOX_ISOLATION=uid-split"));
312 assert!(entrypoint(&GCP_AGENT_PLATFORM).contains("\nUSER 1000:1000\n"));
313 assert!(contract_env(&GCP_AGENT_PLATFORM).contains("ALIEN_SANDBOX_ISOLATION=platform"));
314 }
315}