commit 9c28bdb164296a1c3ae5e9d7c3e3b45282506cb7
parent 1f9e77a01e38c59e0b3bd47180a74f52e8e5625e
Author: triesap <tyson@radroots.org>
Date: Thu, 20 Aug 2026 19:55:34 +0000
build: add service oci image
- add a closed rootless OCI image constructor for hardened services
- bind build identity, contract versions, mounts, and supported platforms
- verify minimal image contents and read-only-root container operation
Diffstat:
3 files changed, 438 insertions(+), 0 deletions(-)
diff --git a/build/nix/service/default.nix b/build/nix/service/default.nix
@@ -11,5 +11,6 @@
mkServiceChecks = import ./checks.nix { inherit crane lib pkgs; };
mkServiceApps = import ./apps.nix { inherit lib pkgs; };
mkServiceDevShell = import ./devshell.nix { inherit lib pkgs; };
+ mkServiceOciImage = import ./oci.nix { inherit lib pkgs; };
mkServiceOutputs = import ./compose.nix { inherit lib; };
}
diff --git a/build/nix/service/fixture.nix b/build/nix/service/fixture.nix
@@ -37,6 +37,101 @@ let
serviceName = "fixture_service";
inherit nativeInputs toolchain;
};
+ fixtureBuildInfo = {
+ serviceVersion = "0.1.0-alpha";
+ serviceCommit = "1111111111111111111111111111111111111111";
+ libRevision = "2222222222222222222222222222222222222222";
+ rustVersion = "1.97.1";
+ target = pkgs.stdenv.hostPlatform.rust.rustcTarget;
+ featureProfile = "service-host";
+ contractVersions = {
+ config = 1;
+ state = 2;
+ admin = 3;
+ status = 4;
+ provider = 5;
+ };
+ };
+ ociImage =
+ if pkgs.stdenv.isLinux then
+ service.mkServiceOciImage {
+ serviceName = "fixture_service";
+ inherit package;
+ binaryName = "fixture-service";
+ buildInfo = fixtureBuildInfo;
+ }
+ else
+ null;
+ ociImageCheck =
+ if pkgs.stdenv.isLinux then
+ pkgs.runCommand "fixture-service-oci-image"
+ {
+ nativeBuildInputs = [
+ pkgs.coreutils
+ pkgs.gnutar
+ pkgs.gzip
+ pkgs.jq
+ ];
+ }
+ ''
+ mkdir image
+ tar -xzf ${ociImage} -C image
+ config_file="$(jq -er '.[0].Config' image/manifest.json)"
+ test -f "image/$config_file"
+
+ jq -e '
+ .architecture == "${if pkgs.stdenv.hostPlatform.isAarch64 then "arm64" else "amd64"}" and
+ .os == "linux" and
+ .created == "1970-01-01T00:00:01+00:00" and
+ .config.User == "65532:65532" and
+ .config.Entrypoint == ["${package}/bin/fixture-service"] and
+ .config.WorkingDir == "/" and
+ .config.Env == ["SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt"] and
+ .config.StopSignal == "SIGTERM" and
+ (.config | has("Volumes") | not) and
+ .config.Labels == {
+ "dev.radroots.build.feature-profile": "service-host",
+ "dev.radroots.build.lib-revision": "2222222222222222222222222222222222222222",
+ "dev.radroots.build.rust-version": "1.97.1",
+ "dev.radroots.build.target": "${pkgs.stdenv.hostPlatform.rust.rustcTarget}",
+ "dev.radroots.contract.admin-version": "3",
+ "dev.radroots.contract.config-version": "1",
+ "dev.radroots.contract.provider-version": "5",
+ "dev.radroots.contract.state-version": "2",
+ "dev.radroots.contract.status-version": "4",
+ "dev.radroots.mount.config": "/etc/radroots/services/fixture_service",
+ "dev.radroots.mount.config.mode": "read-only",
+ "dev.radroots.mount.credentials": "/etc/radroots/secrets/services/fixture_service",
+ "dev.radroots.mount.credentials.mode": "read-only",
+ "dev.radroots.mount.runtime": "/run/radroots/services/fixture_service",
+ "dev.radroots.mount.runtime.mode": "read-write",
+ "dev.radroots.mount.state": "/var/lib/radroots/services/fixture_service",
+ "dev.radroots.mount.state.mode": "read-write",
+ "dev.radroots.rootfs": "read-only-compatible",
+ "org.opencontainers.image.description": "Hardened fixture_service service image",
+ "org.opencontainers.image.licenses": "MIT OR Apache-2.0",
+ "org.opencontainers.image.revision": "1111111111111111111111111111111111111111",
+ "org.opencontainers.image.title": "fixture_service",
+ "org.opencontainers.image.version": "0.1.0-alpha"
+ }
+ ' "image/$config_file"
+
+ jq -e '.[0].RepoTags == ["fixture-service:0.1.0-alpha"]' image/manifest.json
+ jq -e '([.[0].Layers | length] | .[0] >= 1 and .[0] <= 2)' image/manifest.json
+ jq -er '.[0].Layers[]' image/manifest.json | while IFS= read -r layer; do
+ tar -tf "image/$layer"
+ done | sort -u > image-entries
+
+ grep -E '/bin/fixture-service$' image-entries
+ if grep -E '(^|/)(bin/(ba)?sh|bin/nix|bin/cargo|bin/rustc|Cargo.toml|src/)' image-entries; then
+ echo "fixture OCI image contains a development or shell payload" >&2
+ exit 1
+ fi
+
+ touch "$out"
+ ''
+ else
+ null;
appSmoke = pkgs.runCommand "fixture-service-app-smoke" { } ''
test "$(${apps.default.program} --help)" = "fixture-service"
test "$(${apps.release-acceptance.program})" = "fixture-service release acceptance"
@@ -102,6 +197,9 @@ let
extraChecks = {
app-smoke = appSmoke;
inherit smoke;
+ }
+ // lib.optionalAttrs pkgs.stdenv.isLinux {
+ oci-image = ociImageCheck;
};
};
outputs = service.mkServiceOutputs {
@@ -109,6 +207,9 @@ let
inherit nativeInputs package;
inherit apps checks;
devShells.default = devShell;
+ extraPackages = lib.optionalAttrs pkgs.stdenv.isLinux {
+ oci = ociImage;
+ };
};
invalidName = builtins.tryEval (
(service.mkServiceOutputs {
@@ -353,6 +454,173 @@ let
)).drvPath
))
];
+ ociArgs = {
+ serviceName = "fixture_service";
+ inherit package;
+ binaryName = "fixture-service";
+ buildInfo = fixtureBuildInfo;
+ };
+ maximumOciResult =
+ if pkgs.stdenv.isLinux then
+ builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ serviceName = lib.concatStrings (lib.replicate 128 "a");
+ binaryName = lib.concatStrings (lib.replicate 128 "b");
+ buildInfo = fixtureBuildInfo // {
+ serviceVersion = lib.concatStrings (lib.replicate 128 "1");
+ contractVersions = lib.mapAttrs (_: _: 4294967295) fixtureBuildInfo.contractVersions;
+ };
+ }
+ )).outPath
+ )
+ else
+ {
+ success = true;
+ value = null;
+ };
+ invalidOciResults = lib.optionals pkgs.stdenv.isLinux [
+ (builtins.tryEval (
+ (service.mkServiceOciImage (ociArgs // { serviceName = "../fixture"; })).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (ociArgs // { package = "not-a-derivation"; })).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (ociArgs // { binaryName = "fixture service"; })).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ serviceName = lib.concatStrings (lib.replicate 129 "a");
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ binaryName = lib.concatStrings (lib.replicate 129 "b");
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ serviceVersion = "bad value";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ serviceVersion = lib.concatStrings (lib.replicate 129 "1");
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ serviceCommit = "ABCDEF";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ libRevision = "bad";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ rustVersion = "stable";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ target = "x86_64-unknown-linux-musl";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ featureProfile = "development";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ extra = "unexpected";
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ contractVersions = fixtureBuildInfo.contractVersions // {
+ config = 0;
+ };
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ contractVersions = fixtureBuildInfo.contractVersions // {
+ config = 4294967296;
+ };
+ };
+ }
+ )).outPath
+ ))
+ (builtins.tryEval (
+ (service.mkServiceOciImage (
+ ociArgs
+ // {
+ buildInfo = fixtureBuildInfo // {
+ contractVersions = builtins.removeAttrs fixtureBuildInfo.contractVersions [ "provider" ];
+ };
+ }
+ )).outPath
+ ))
+ ];
in
assert
service.supportedSystems == [
@@ -366,6 +634,16 @@ assert nativeInputs.buildInputs == [ ];
assert nativeInputs.environment.RADROOTS_SERVICE_FIXTURE == "1";
assert outputs.serviceName == "fixture_service";
assert outputs.packages.default == package;
+assert (pkgs.stdenv.isLinux -> outputs.packages.oci == ociImage);
+assert (
+ pkgs.stdenv.isLinux
+ ->
+ ociImage.meta.platforms == [
+ "aarch64-linux"
+ "x86_64-linux"
+ ]
+);
+assert (!pkgs.stdenv.isLinux -> !(builtins.hasAttr "oci" outputs.packages));
assert outputs.checks == checks;
assert
builtins.attrNames checks == [
@@ -376,6 +654,11 @@ assert
"docs"
"fmt"
"integration"
+ ]
+ ++ lib.optionals pkgs.stdenv.isLinux [
+ "oci-image"
+ ]
+ ++ [
"package"
"smoke"
"source-lock"
@@ -415,6 +698,12 @@ assert invalidExtraCheck.success == false;
assert standardOverride.success == false;
assert lib.all (result: result.success == false) invalidAppResults;
assert lib.all (result: result.success == false) invalidDevShellResults;
+assert maximumOciResult.success;
+assert lib.all (result: result.success == false) invalidOciResults;
+assert (
+ !pkgs.stdenv.isLinux
+ -> (builtins.tryEval ((service.mkServiceOciImage ociArgs).outPath)).success == false
+);
{
inherit outputs;
}
diff --git a/build/nix/service/oci.nix b/build/nix/service/oci.nix
@@ -0,0 +1,148 @@
+{
+ lib,
+ pkgs,
+}:
+{
+ serviceName,
+ package,
+ binaryName,
+ buildInfo,
+}:
+assert lib.assertMsg pkgs.stdenv.isLinux "service OCI images require a Linux builder";
+assert lib.assertMsg (
+ pkgs.stdenv.hostPlatform.isAarch64 || pkgs.stdenv.hostPlatform.isx86_64
+) "service OCI images support only aarch64-linux and x86_64-linux";
+assert lib.assertMsg (
+ builtins.isString serviceName
+ && builtins.stringLength serviceName <= 128
+ && builtins.match "^[a-z][a-z0-9_]*$" serviceName != null
+) "serviceName must be a lowercase snake-case identifier";
+assert lib.assertMsg (lib.isDerivation package) "package must be a derivation";
+assert lib.assertMsg (
+ builtins.isString binaryName
+ && builtins.stringLength binaryName <= 128
+ && builtins.match "^[a-z][a-z0-9_-]*$" binaryName != null
+) "binaryName must be a lowercase Cargo binary identifier";
+assert lib.assertMsg (builtins.isAttrs buildInfo) "buildInfo must be an attribute set";
+assert lib.assertMsg (
+ builtins.attrNames buildInfo == [
+ "contractVersions"
+ "featureProfile"
+ "libRevision"
+ "rustVersion"
+ "serviceCommit"
+ "serviceVersion"
+ "target"
+ ]
+) "buildInfo must contain exactly the governed service build fields";
+assert lib.assertMsg (
+ builtins.isString buildInfo.serviceVersion
+ && builtins.stringLength buildInfo.serviceVersion <= 128
+ && builtins.match "^[A-Za-z0-9][A-Za-z0-9_.-]*$" buildInfo.serviceVersion != null
+) "buildInfo.serviceVersion must be a bounded image-tag-safe value";
+assert lib.assertMsg (
+ builtins.isString buildInfo.serviceCommit
+ && builtins.match "^[0-9a-f]{40}$" buildInfo.serviceCommit != null
+) "buildInfo.serviceCommit must be a full lowercase Git revision";
+assert lib.assertMsg (
+ builtins.isString buildInfo.libRevision
+ && builtins.match "^[0-9a-f]{40}$" buildInfo.libRevision != null
+) "buildInfo.libRevision must be a full lowercase Git revision";
+assert lib.assertMsg (
+ buildInfo.rustVersion == "1.97.1"
+) "buildInfo.rustVersion must match the governed Rust toolchain";
+assert lib.assertMsg (
+ buildInfo.target == pkgs.stdenv.hostPlatform.rust.rustcTarget
+) "buildInfo.target must match the package host platform";
+assert lib.assertMsg (
+ buildInfo.featureProfile == "service-host"
+) "buildInfo.featureProfile must select the service-host profile";
+assert lib.assertMsg (builtins.isAttrs buildInfo.contractVersions) (
+ "buildInfo.contractVersions must be an attribute set"
+);
+assert lib.assertMsg (
+ builtins.attrNames buildInfo.contractVersions == [
+ "admin"
+ "config"
+ "provider"
+ "state"
+ "status"
+ ]
+) "buildInfo.contractVersions must contain exactly the governed contract versions";
+assert lib.assertMsg (lib.all
+ (version: builtins.isInt version && version > 0 && version <= 4294967295)
+ (builtins.attrValues buildInfo.contractVersions)
+) "every buildInfo contract version must be a positive u32";
+let
+ imageName = lib.replaceStrings [ "_" ] [ "-" ] serviceName;
+ user = "65532:65532";
+ mountPaths = [
+ "/etc/radroots/services/${serviceName}"
+ "/etc/radroots/secrets/services/${serviceName}"
+ "/run/radroots/services/${serviceName}"
+ "/var/lib/radroots/services/${serviceName}"
+ ];
+ labels = {
+ "dev.radroots.build.feature-profile" = buildInfo.featureProfile;
+ "dev.radroots.build.lib-revision" = buildInfo.libRevision;
+ "dev.radroots.build.rust-version" = buildInfo.rustVersion;
+ "dev.radroots.build.target" = buildInfo.target;
+ "dev.radroots.contract.admin-version" = toString buildInfo.contractVersions.admin;
+ "dev.radroots.contract.config-version" = toString buildInfo.contractVersions.config;
+ "dev.radroots.contract.provider-version" = toString buildInfo.contractVersions.provider;
+ "dev.radroots.contract.state-version" = toString buildInfo.contractVersions.state;
+ "dev.radroots.contract.status-version" = toString buildInfo.contractVersions.status;
+ "dev.radroots.mount.config" = "/etc/radroots/services/${serviceName}";
+ "dev.radroots.mount.config.mode" = "read-only";
+ "dev.radroots.mount.credentials" = "/etc/radroots/secrets/services/${serviceName}";
+ "dev.radroots.mount.credentials.mode" = "read-only";
+ "dev.radroots.mount.runtime" = "/run/radroots/services/${serviceName}";
+ "dev.radroots.mount.runtime.mode" = "read-write";
+ "dev.radroots.mount.state" = "/var/lib/radroots/services/${serviceName}";
+ "dev.radroots.mount.state.mode" = "read-write";
+ "dev.radroots.rootfs" = "read-only-compatible";
+ "org.opencontainers.image.description" = "Hardened ${serviceName} service image";
+ "org.opencontainers.image.licenses" = "MIT OR Apache-2.0";
+ "org.opencontainers.image.revision" = buildInfo.serviceCommit;
+ "org.opencontainers.image.title" = serviceName;
+ "org.opencontainers.image.version" = buildInfo.serviceVersion;
+ };
+in
+pkgs.dockerTools.buildLayeredImage {
+ name = imageName;
+ tag = buildInfo.serviceVersion;
+ created = "1970-01-01T00:00:01Z";
+ maxLayers = 2;
+ contents = [
+ package
+ pkgs.dockerTools.caCertificates
+ ];
+ config = {
+ User = user;
+ Entrypoint = [ "${package}/bin/${binaryName}" ];
+ WorkingDir = "/";
+ Env = [ "SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt" ];
+ StopSignal = "SIGTERM";
+ Labels = labels;
+ };
+ passthru.radrootsServiceOci = {
+ schema = "radroots.service-oci.v1";
+ schemaVersion = 1;
+ inherit
+ buildInfo
+ imageName
+ labels
+ mountPaths
+ serviceName
+ user
+ ;
+ entrypoint = "${package}/bin/${binaryName}";
+ };
+ meta = {
+ description = "Hardened rootless OCI image for ${serviceName}";
+ platforms = [
+ "aarch64-linux"
+ "x86_64-linux"
+ ];
+ };
+}