pub const AGENT_PATH: &str = "/usr/local/bin/alien-sandbox-agent";
pub const AGENT_PORT: u16 = 8971;
pub const AGENT_MODE: u32 = 0o755;
pub const EXEC_USER: &str = "sandbox";
pub const SESSION_ROOT_MODE: u32 = 0o700;
pub const GCP_AGENT_LOG_FILTER: &str = "info";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Isolation {
UidSplit,
Platform,
}
impl Isolation {
pub fn env_value(self) -> &'static str {
match self {
Self::UidSplit => "uid-split",
Self::Platform => "platform",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Authorization {
Transport,
Capability,
}
impl Authorization {
pub fn env_value(self) -> &'static str {
match self {
Self::Transport => "transport",
Self::Capability => "capability",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SandboxImage {
pub exec_uid: u32,
pub session_root: &'static str,
pub port: u16,
pub authorization: Authorization,
pub isolation: Isolation,
}
impl SandboxImage {
pub fn exec_gid(&self) -> u32 {
self.exec_uid
}
pub fn contract_env_vars(&self) -> [(&'static str, String); 6] {
[
("ALIEN_SANDBOX_ROOT", self.session_root.to_string()),
("ALIEN_SANDBOX_PORT", self.port.to_string()),
(
"ALIEN_SANDBOX_AUTHORIZATION",
self.authorization.env_value().to_string(),
),
("ALIEN_SANDBOX_EXEC_UID", self.exec_uid.to_string()),
("ALIEN_SANDBOX_EXEC_GID", self.exec_gid().to_string()),
(
"ALIEN_SANDBOX_ISOLATION",
self.isolation.env_value().to_string(),
),
]
}
pub fn user(&self) -> Option<String> {
match self.isolation {
Isolation::UidSplit => None,
Isolation::Platform => Some(format!("{}:{}", self.exec_uid, self.exec_gid())),
}
}
pub fn exposed_port(&self) -> String {
format!("{}/tcp", self.port)
}
pub fn passwd_entry(&self) -> String {
format!(
"{EXEC_USER}:x:{uid}:{gid}::{root}:/sbin/nologin",
uid = self.exec_uid,
gid = self.exec_gid(),
root = self.session_root,
)
}
pub fn group_entry(&self) -> String {
format!("{EXEC_USER}:x:{gid}:", gid = self.exec_gid())
}
}
pub const AWS_MICROVM: SandboxImage = SandboxImage {
exec_uid: 60000,
session_root: "/sandbox",
port: AGENT_PORT,
authorization: Authorization::Transport,
isolation: Isolation::UidSplit,
};
pub const GCP_AGENT_PLATFORM: SandboxImage = SandboxImage {
exec_uid: 1000,
session_root: "/sandbox",
port: 8080,
authorization: Authorization::Transport,
isolation: Isolation::Platform,
};
pub fn gcp_agent_platform_env() -> Vec<(&'static str, String)> {
let mut env = GCP_AGENT_PLATFORM.contract_env_vars().to_vec();
env.push(("RUST_LOG", GCP_AGENT_LOG_FILTER.to_string()));
env
}
pub const DEFAULT_SANDBOX_BASE_IMAGE: &str = "public.ecr.aws/docker/library/buildpack-deps:26.04@sha256:159ea382e6fb39e62480ee932113f885f7bd787cd4895fc4dc71aebb175077fd";
pub const DEFAULT_SANDBOX_UV_IMAGE: &str = "ghcr.io/astral-sh/uv:0.12.21@sha256:a7aed3216253ee804de3e2d8afa5073baa1a177335345d43845cd4165e43b711";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SandboxTool {
pub package: Option<&'static str>,
pub version_command: &'static str,
}
pub const DEFAULT_SANDBOX_TOOLS: &[SandboxTool] = &[
SandboxTool {
package: Some("nftables"),
version_command: "/usr/sbin/nft --version",
},
SandboxTool {
package: Some("iptables"),
version_command: "iptables-nft --version",
},
SandboxTool {
package: Some("nodejs"),
version_command: "node --version",
},
SandboxTool {
package: Some("npm"),
version_command: "npm --version",
},
SandboxTool {
package: Some("ripgrep"),
version_command: "rg --version",
},
SandboxTool {
package: Some("jq"),
version_command: "jq --version",
},
SandboxTool {
package: Some("zip"),
version_command: "zip -v",
},
SandboxTool {
package: Some("less"),
version_command: "less --version",
},
SandboxTool {
package: Some("nano"),
version_command: "nano --version",
},
SandboxTool {
package: Some("vim-tiny"),
version_command: "vim.tiny --version",
},
SandboxTool {
package: Some("htop"),
version_command: "htop --version",
},
SandboxTool {
package: None,
version_command: "uv --version",
},
SandboxTool {
package: None,
version_command: "uvx --version",
},
];
pub fn identity_setup(image: &SandboxImage) -> String {
format!(
r#"RUN printf '{passwd}\n' >> /etc/passwd \
&& printf '{group}\n' >> /etc/group \
&& mkdir -p {root} \
&& chown {uid}:{gid} {root} \
&& chmod {SESSION_ROOT_MODE:04o} {root}"#,
passwd = image.passwd_entry(),
group = image.group_entry(),
root = image.session_root,
uid = image.exec_uid,
gid = image.exec_gid(),
)
}
pub fn contract_env(image: &SandboxImage) -> String {
let vars: Vec<String> = image
.contract_env_vars()
.iter()
.map(|(name, value)| format!("{name}={value}"))
.collect();
format!("ENV {}", vars.join(" \\\n "))
}
pub fn entrypoint(image: &SandboxImage) -> String {
let ending = match image.user() {
None => String::new(),
Some(user) => format!(
"# Explicit gid so a runtime that does not read /etc/passwd cannot start the agent in \
group 0, which\n# makes the exec drop a privilege crossing whose setgroups needs a \
CAP_SETGID this image lacks, so\n# every exec fails.\nUSER {user}\n"
),
};
format!(
"EXPOSE {port}\n{ending}ENTRYPOINT [\"{AGENT_PATH}\"]",
port = image.exposed_port()
)
}
#[cfg(test)]
const GCP_DOCKERFILE: &str = "docker/Dockerfile.alien-sandbox-agent";
#[cfg(test)]
const GCP_DEFAULT_DOCKERFILE: &str = "docker/Dockerfile.alien-sandbox-gcp";
#[cfg(test)]
const DEFAULT_SANDBOX_DOCKERFILE: &str = "docker/Dockerfile.alien-sandbox-default";
#[cfg(test)]
const DEFAULT_SANDBOX_TOOLS_FILE: &str = "docker/sandbox-default-tools.txt";
#[cfg(test)]
const SANDBOX_FILES_UPDATE: &str = "UPDATE_SANDBOX_AGENT_DOCKERFILE";
#[cfg(test)]
fn default_sandbox_dockerfile() -> String {
let packages: Vec<&str> = DEFAULT_SANDBOX_TOOLS
.iter()
.filter_map(|tool| tool.package)
.collect();
format!(
r#"# Generated by `cargo test -p alien-core --lib sandbox_image`. Do not edit by hand.
# Regenerate with {SANDBOX_FILES_UPDATE}=1 in front of that command.
#
# Multi-arch tools-only base for the default sandbox images, with no agent. An image that runs the
# agent adds it and its own ending on top, and a runtime with no in-guest agent runs this as is,
# so it declares no USER, ENTRYPOINT or ENV.
FROM {DEFAULT_SANDBOX_BASE_IMAGE}
# No version pins: the -updates and -security pockets supersede a pinned version and the arm64
# ports lag behind, so a pin breaks the build. The published digest is what fixes the versions.
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
{packages} \
&& rm -rf /var/lib/apt/lists/*
COPY --from={DEFAULT_SANDBOX_UV_IMAGE} /uv /uvx /usr/local/bin/
"#,
packages = packages.join(" "),
)
}
#[cfg(test)]
fn default_sandbox_tools_list() -> String {
let mut list = format!(
"# Generated by `cargo test -p alien-core --lib sandbox_image`. Do not edit by hand.
# Regenerate with {SANDBOX_FILES_UPDATE}=1 in front of that command.
#
# One command per tool the default sandbox base ships; each prints a version and exits zero.
"
);
for tool in DEFAULT_SANDBOX_TOOLS {
list.push_str(tool.version_command);
list.push('\n');
}
list
}
#[cfg(test)]
enum GcpBase {
Image(&'static str),
BuildArg(&'static str),
}
#[cfg(test)]
fn gcp_agent_platform_dockerfile() -> String {
gcp_dockerfile(
"Multi-arch build for the alien-sandbox-agent Docker image",
GcpBase::Image("docker.io/chainguard/wolfi-base:latest"),
"# git is for the sandboxed command, not the agent, and pulls 24 transitive packages. That cost
# lands here because this image is the sandbox, with no customer base image underneath to carry it.
RUN apk add --no-cache git",
)
}
#[cfg(test)]
fn gcp_default_sandbox_dockerfile() -> String {
assert_eq!(
GCP_AGENT_PLATFORM.exec_uid, 1000,
"the userdel below exists only because Ubuntu's own user holds the exec uid"
);
gcp_dockerfile(
"Multi-arch build for the default GCP sandbox image: the default sandbox base plus the agent",
GcpBase::BuildArg("SANDBOX_DEFAULT_BASE"),
"# Ubuntu ships `ubuntu` at uid 1000. Appending a second entry for that uid leaves `id` and every
# tool resolving it to `ubuntu`, so the exec user would not be `sandbox`.
RUN userdel --remove ubuntu",
)
}
#[cfg(test)]
fn gcp_dockerfile(title: &str, base: GcpBase, base_setup: &str) -> String {
let image = &GCP_AGENT_PLATFORM;
let (directive, base_arg, base) = match base {
GcpBase::Image(reference) => (String::new(), String::new(), reference.to_string()),
GcpBase::BuildArg(name) => (
"# check=skip=InvalidDefaultArgInFrom\n".to_string(),
format!(
"# No default: the base's digest exists only once it is built, so whoever builds \
this image\n# passes it. The check directive on line 1 is for this.\nARG {name}\n\n"
),
format!("${{{name}}}"),
),
};
format!(
r#"{directive}# Generated by `cargo test -p alien-core --lib sandbox_image`. Do not edit by hand.
# Regenerate with {SANDBOX_FILES_UPDATE}=1 in front of that command.
#
# {title}
# Run directly as the GCP Agent Platform sandbox; nothing layers on top of it
{base_arg}FROM docker.io/chainguard/wolfi-base:latest AS binary-selector
COPY target/aarch64-unknown-linux-musl/release/alien-sandbox-agent /tmp/alien-sandbox-agent-aarch64
COPY target/x86_64-unknown-linux-musl/release/alien-sandbox-agent /tmp/alien-sandbox-agent-x86_64
ARG TARGETARCH
RUN case "$TARGETARCH" in \
amd64) cp /tmp/alien-sandbox-agent-x86_64 /tmp/alien-sandbox-agent ;; \
arm64) cp /tmp/alien-sandbox-agent-aarch64 /tmp/alien-sandbox-agent ;; \
*) echo "unsupported TARGETARCH '$TARGETARCH'" >&2; exit 1 ;; \
esac
FROM {base}
{base_setup}
# Root-owned and unwritable by uid {exec_uid}: the supervised command runs under that uid and must not
# be able to rewrite its own supervisor.
COPY --from=binary-selector --chown=0:0 --chmod={AGENT_MODE:04o} \
/tmp/alien-sandbox-agent {AGENT_PATH}
# Numeric ids and a plain append rather than adduser, which differs across base distributions.
# Linux runs a process under a uid with no passwd entry, but tooling inside the sandbox reads one.
{identity}
# The template carries no env, so the contract lives here, and none of it is optional. transport
# serves an uncapabilitied request only from a socket `peer::transport_may_serve` cannot trace
# back to the exec uid, and the agent refuses the mode where /proc/net/tcp is unreadable.
{env}
# The release build resolves tracing-subscriber once across every package it names, and five of
# them ask for env-filter, so the agent's fmt::init() has an EnvFilter under it. Unset, that
# filter discards the startup warning saying this image serves requests without a capability.
ENV RUST_LOG={GCP_AGENT_LOG_FILTER}
{entrypoint}
"#,
exec_uid = image.exec_uid,
identity = identity_setup(image),
env = contract_env(image),
entrypoint = entrypoint(image),
)
}
#[cfg(test)]
mod tests {
use super::*;
fn committed_path(relative: &str) -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join(relative)
}
fn first_difference(committed: &str, rendered: &str) -> String {
for (index, (left, right)) in committed.lines().zip(rendered.lines()).enumerate() {
if left != right {
let line = index + 1;
return format!("line {line}: committed {left:?}, contract renders {right:?}");
}
}
format!(
"committed has {} lines, the contract renders {}",
committed.lines().count(),
rendered.lines().count()
)
}
#[test]
fn the_committed_gcp_dockerfiles_are_what_the_contract_renders() {
for (file, rendered) in [
(GCP_DOCKERFILE, gcp_agent_platform_dockerfile()),
(GCP_DEFAULT_DOCKERFILE, gcp_default_sandbox_dockerfile()),
(DEFAULT_SANDBOX_DOCKERFILE, default_sandbox_dockerfile()),
(DEFAULT_SANDBOX_TOOLS_FILE, default_sandbox_tools_list()),
] {
let path = committed_path(file);
if std::env::var_os(SANDBOX_FILES_UPDATE).is_some() {
std::fs::write(&path, &rendered)
.unwrap_or_else(|error| panic!("{} must be writable: {error}", path.display()));
continue;
}
let committed = std::fs::read_to_string(&path)
.unwrap_or_else(|error| panic!("{} must be readable: {error}", path.display()));
assert!(
committed == rendered,
"{file} has drifted from the contract it is rendered from.\n\
{}\n\
Regenerate it: {SANDBOX_FILES_UPDATE}=1 cargo test -p alien-core --lib sandbox_image",
first_difference(&committed, &rendered)
);
}
}
#[test]
fn the_gcp_env_accessor_is_every_env_the_committed_dockerfile_sets() {
let committed = std::fs::read_to_string(committed_path(GCP_DEFAULT_DOCKERFILE)).unwrap();
let mut set = Vec::new();
let mut in_env = false;
for line in committed.lines() {
let line = line.trim();
let rest = match line.strip_prefix("ENV ") {
Some(rest) => rest,
None if in_env => line,
None => continue,
};
in_env = rest.ends_with('\\');
for pair in rest.trim_end_matches('\\').split_whitespace() {
let (name, value) = pair.split_once('=').expect("ENV name=value");
set.push((name.to_string(), value.to_string()));
}
}
let accessor: Vec<(String, String)> = gcp_agent_platform_env()
.into_iter()
.map(|(name, value)| (name.to_string(), value))
.collect();
assert_eq!(accessor, set);
}
#[test]
fn the_two_images_carry_the_identities_their_stacks_were_built_against() {
assert_eq!(AWS_MICROVM.port, 8971);
assert_eq!(AWS_MICROVM.exec_uid, 60000);
assert_eq!(AWS_MICROVM.session_root, "/sandbox");
assert_eq!(GCP_AGENT_PLATFORM.port, 8080);
assert_eq!(GCP_AGENT_PLATFORM.exec_uid, 1000);
assert_eq!(GCP_AGENT_PLATFORM.session_root, "/sandbox");
for image in [&AWS_MICROVM, &GCP_AGENT_PLATFORM] {
assert_ne!(image.exec_uid, 0, "the exec uid must never be root");
}
}
#[test]
fn the_ending_an_image_declares_follows_its_isolation() {
assert!(!entrypoint(&AWS_MICROVM).contains("USER "));
assert!(contract_env(&AWS_MICROVM).contains("ALIEN_SANDBOX_ISOLATION=uid-split"));
assert!(entrypoint(&GCP_AGENT_PLATFORM).contains("\nUSER 1000:1000\n"));
assert!(contract_env(&GCP_AGENT_PLATFORM).contains("ALIEN_SANDBOX_ISOLATION=platform"));
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SandboxImageCommand {
pub command: Vec<String>,
pub env: std::collections::BTreeMap<String, String>,
pub working_directory: String,
}
pub const IMAGE_COMMAND_PATH: &str = "/opt/alien/image-command.json";