nixos/virtualisation/nspawn-container: add virtualisation.credentials support (#535258)
This commit is contained in:
@@ -1688,6 +1688,7 @@ class NspawnMachine(BaseMachine):
|
||||
|
||||
self.process = subprocess.Popen(
|
||||
[self.start_command],
|
||||
cwd=self.state_dir,
|
||||
env={
|
||||
"RUN_NSPAWN_ROOT_DIR": str(self.state_dir),
|
||||
"RUN_NSPAWN_SHARED_DIR": str(self.shared_dir),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
{ lib, pkgs, ... }:
|
||||
{
|
||||
options.virtualisation.credentials = lib.mkOption {
|
||||
description = ''
|
||||
Credentials to pass to the VM or container using systemd's credential system.
|
||||
|
||||
See {manpage}`systemd.exec(5)`, {manpage}`systemd-creds(1)` and https://systemd.io/CREDENTIALS/ for more
|
||||
information about systemd credentials.
|
||||
'';
|
||||
default = { };
|
||||
example = lib.literalExpression ''
|
||||
{
|
||||
database-password = {
|
||||
text = "my-secret-password";
|
||||
};
|
||||
ssl-cert = {
|
||||
source = "./cert.pem";
|
||||
};
|
||||
binary-key = {
|
||||
source = "./private.der";
|
||||
};
|
||||
}
|
||||
'';
|
||||
type = lib.types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{
|
||||
name,
|
||||
options,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options = {
|
||||
source = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.pathWith { });
|
||||
default = null;
|
||||
description = ''
|
||||
Source file on the host containing the credential data.
|
||||
'';
|
||||
};
|
||||
text = lib.mkOption {
|
||||
default = null;
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = ''
|
||||
Text content of the credential.
|
||||
|
||||
For binary data or when the credential content should come from
|
||||
an existing file, use `source` instead.
|
||||
|
||||
::: {.warning}
|
||||
The text here is stored in the host's nix store as a file.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
};
|
||||
config.source = lib.mkIf (config.text != null) (
|
||||
lib.mkDerivedConfig options.text (pkgs.writeText name)
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -21,6 +21,10 @@ let
|
||||
cfg = config.virtualisation;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
../credentials-options.nix
|
||||
];
|
||||
|
||||
options = {
|
||||
|
||||
virtualisation.cmdline = lib.mkOption {
|
||||
@@ -131,7 +135,8 @@ in
|
||||
|
||||
# Send a READY=1 notification to a socket when the container is fully booted.
|
||||
"--notify-ready=yes"
|
||||
];
|
||||
]
|
||||
++ lib.mapAttrsToList (name: cred: "--load-credential=${name}:${cred.source}") cfg.credentials;
|
||||
|
||||
system.build.nspawn =
|
||||
let
|
||||
|
||||
@@ -399,6 +399,7 @@ in
|
||||
imports = [
|
||||
../profiles/qemu-guest.nix
|
||||
./disk-size-option.nix
|
||||
./credentials-options.nix
|
||||
(mkRenamedOptionModule
|
||||
[
|
||||
"virtualisation"
|
||||
@@ -1126,81 +1127,20 @@ in
|
||||
};
|
||||
|
||||
virtualisation.credentials = mkOption {
|
||||
description = ''
|
||||
Credentials to pass to the VM using systemd's credential system.
|
||||
|
||||
See {manpage}`systemd.exec(5)` , {manpage}`systemd-creds(1)` and https://systemd.io/CREDENTIALS/ for more
|
||||
information about systemd credentials.
|
||||
'';
|
||||
default = { };
|
||||
example = {
|
||||
database-password = {
|
||||
text = "my-secret-password";
|
||||
};
|
||||
ssl-cert = {
|
||||
source = "./cert.pem";
|
||||
};
|
||||
binary-key = {
|
||||
mechanism = "fw_cfg";
|
||||
source = "./private.der";
|
||||
};
|
||||
config-file = {
|
||||
mechanism = "smbios";
|
||||
text = ''
|
||||
[database]
|
||||
host=localhost
|
||||
port=5432
|
||||
'';
|
||||
};
|
||||
};
|
||||
type = types.attrsOf (
|
||||
lib.types.submodule (
|
||||
{
|
||||
name,
|
||||
options,
|
||||
config,
|
||||
...
|
||||
}:
|
||||
{
|
||||
options = {
|
||||
mechanism = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"fw_cfg"
|
||||
"smbios"
|
||||
];
|
||||
default = if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg";
|
||||
defaultText = lib.literalExpression ''if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"'';
|
||||
description = ''
|
||||
The mechanism used to pass the credential to the VM.
|
||||
'';
|
||||
};
|
||||
source = lib.mkOption {
|
||||
type = lib.types.nullOr (lib.types.pathWith { });
|
||||
default = null;
|
||||
description = ''
|
||||
Source file on the host containing the credential data.
|
||||
'';
|
||||
};
|
||||
text = lib.mkOption {
|
||||
default = null;
|
||||
type = lib.types.nullOr lib.types.str;
|
||||
description = ''
|
||||
Text content of the credential.
|
||||
|
||||
For binary data or when the credential content should come from
|
||||
an existing file, use `source` instead.
|
||||
|
||||
::: {.warning}
|
||||
The text here is stored in the host's nix store as a file.
|
||||
:::
|
||||
'';
|
||||
};
|
||||
};
|
||||
config.source = lib.mkIf (config.text != null) (
|
||||
lib.mkDerivedConfig options.text (pkgs.writeText name)
|
||||
);
|
||||
}
|
||||
)
|
||||
lib.types.submodule {
|
||||
options.mechanism = lib.mkOption {
|
||||
type = lib.types.enum [
|
||||
"fw_cfg"
|
||||
"smbios"
|
||||
];
|
||||
default = if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg";
|
||||
defaultText = lib.literalExpression ''if pkgs.stdenv.hostPlatform.isx86 then "smbios" else "fw_cfg"'';
|
||||
description = ''
|
||||
The mechanism used to pass the credential to the VM.
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -446,6 +446,14 @@ in
|
||||
};
|
||||
coturn = runTest ./coturn.nix;
|
||||
couchdb = runTest ./couchdb.nix;
|
||||
credentials-fwcfg = runTest {
|
||||
imports = [ ./credentials.nix ];
|
||||
_module.args.mechanism = "fw_cfg";
|
||||
};
|
||||
credentials-smbios = runTestOn [ "x86_64-linux" ] {
|
||||
imports = [ ./credentials.nix ];
|
||||
_module.args.mechanism = "smbios";
|
||||
};
|
||||
cri-o = runTestOn [ "aarch64-linux" "x86_64-linux" ] ./cri-o.nix;
|
||||
croc = runTest ./croc.nix;
|
||||
cross-seed = runTest ./cross-seed.nix;
|
||||
@@ -1415,14 +1423,6 @@ in
|
||||
pykms = runTest ./pykms.nix;
|
||||
qbittorrent = runTest ./qbittorrent.nix;
|
||||
qboot = runTestOn [ "x86_64-linux" "i686-linux" ] ./qboot.nix;
|
||||
qemu-vm-credentials-fwcfg = runTest {
|
||||
imports = [ ./qemu-vm-credentials.nix ];
|
||||
_module.args.mechanism = "fw_cfg";
|
||||
};
|
||||
qemu-vm-credentials-smbios = runTestOn [ "x86_64-linux" ] {
|
||||
imports = [ ./qemu-vm-credentials.nix ];
|
||||
_module.args.mechanism = "smbios";
|
||||
};
|
||||
qemu-vm-external-disk-image = runTest ./qemu-vm-external-disk-image.nix;
|
||||
qemu-vm-restrictnetwork = handleTest ./qemu-vm-restrictnetwork.nix { };
|
||||
qemu-vm-store = runTest ./qemu-vm-store.nix;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
mechanism,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
secret = ''
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
'';
|
||||
secret-file = "bar";
|
||||
|
||||
common-credentials = {
|
||||
secret-default-mechanism = {
|
||||
text = "default-mechanism";
|
||||
};
|
||||
secret-file-nix-store = {
|
||||
source = pkgs.writeText "secret-file-nix-store" secret-file;
|
||||
};
|
||||
secret-file-host = {
|
||||
source = "./secret-file-host";
|
||||
};
|
||||
secret-file-host-binary = {
|
||||
source = "./secret-file-host-binary";
|
||||
};
|
||||
};
|
||||
in
|
||||
|
||||
{
|
||||
name = "credentials-${mechanism}";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ arianvp ];
|
||||
|
||||
# No VM<->container traffic in this test; credentials are static.
|
||||
requiredFeatures.devnet = lib.mkForce false;
|
||||
|
||||
nodes.vm = {
|
||||
virtualisation.credentials = common-credentials // {
|
||||
secret = {
|
||||
inherit mechanism;
|
||||
text = secret;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
containers.container = {
|
||||
virtualisation.credentials = common-credentials // {
|
||||
secret = {
|
||||
text = secret;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import base64
|
||||
|
||||
secret_file_host = "baz"
|
||||
# Binary data with null bytes, high bytes, and other problematic characters.
|
||||
secret_file_host_binary = bytes([
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
0xDE, 0xAD, 0xBE, 0xEF,
|
||||
0xFF, 0xFE, 0xFD, 0xFC,
|
||||
0x00, 0x00, 0x00, 0x00,
|
||||
0x80, 0x81, 0x82, 0x83,
|
||||
])
|
||||
|
||||
def assert_credentials(m):
|
||||
with open(m.state_dir / "secret-file-host", "w") as f:
|
||||
f.write(secret_file_host)
|
||||
with open(m.state_dir / "secret-file-host-binary", "wb") as f2:
|
||||
f2.write(secret_file_host_binary)
|
||||
|
||||
t.assertEqual(
|
||||
m.succeed("systemd-creds --system cat secret").strip(),
|
||||
"foo\nbar\nbaz",
|
||||
)
|
||||
t.assertEqual(
|
||||
m.succeed("systemd-creds --system cat secret-default-mechanism").strip(),
|
||||
"default-mechanism",
|
||||
)
|
||||
t.assertEqual(
|
||||
m.succeed("systemd-creds --system cat secret-file-nix-store").strip(),
|
||||
"${secret-file}",
|
||||
)
|
||||
t.assertEqual(
|
||||
m.succeed("systemd-creds --system cat secret-file-host").strip(),
|
||||
secret_file_host,
|
||||
)
|
||||
result = m.succeed(
|
||||
"systemd-creds --system cat secret-file-host-binary --transcode=base64"
|
||||
).strip()
|
||||
expected = base64.b64encode(secret_file_host_binary).decode("ascii")
|
||||
t.assertEqual(
|
||||
result,
|
||||
expected,
|
||||
f"Binary credential mismatch: got {result}, expected {expected}",
|
||||
)
|
||||
|
||||
assert_credentials(vm)
|
||||
assert_credentials(container)
|
||||
'';
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
{
|
||||
lib,
|
||||
pkgs,
|
||||
mechanism,
|
||||
...
|
||||
}:
|
||||
|
||||
let
|
||||
secret = ''
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
'';
|
||||
secret-file = "bar";
|
||||
in
|
||||
|
||||
{
|
||||
name = "qemu-vm-credentials-${mechanism}";
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ arianvp ];
|
||||
|
||||
nodes = {
|
||||
machine = {
|
||||
virtualisation.credentials = {
|
||||
secret = {
|
||||
inherit mechanism;
|
||||
text = secret;
|
||||
};
|
||||
secret-default-mechanism = {
|
||||
text = "default-mechanism";
|
||||
};
|
||||
secret-file-nix-store = {
|
||||
inherit mechanism;
|
||||
source = pkgs.writeText "secret-file-nix-store" secret-file;
|
||||
};
|
||||
secret-file-host = {
|
||||
inherit mechanism;
|
||||
source = "./secret-file-host";
|
||||
};
|
||||
secret-file-host-binary = {
|
||||
inherit mechanism;
|
||||
source = "./secret-file-host-binary";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
testScript = ''
|
||||
import base64
|
||||
secret_file_host = "baz"
|
||||
# Binary data with null bytes, high bytes, and all sorts of problematic characters
|
||||
secret_file_host_binary = bytes([
|
||||
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, # null and control chars
|
||||
0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F,
|
||||
0xDE, 0xAD, 0xBE, 0xEF, # classic binary pattern
|
||||
0xFF, 0xFE, 0xFD, 0xFC, # high bytes
|
||||
0x00, 0x00, 0x00, 0x00, # multiple nulls
|
||||
0x80, 0x81, 0x82, 0x83, # more high bytes
|
||||
])
|
||||
|
||||
with open(machine.state_dir / "secret-file-host", "w") as f:
|
||||
f.write(secret_file_host)
|
||||
with open(machine.state_dir / "secret-file-host-binary", "wb") as f2:
|
||||
f2.write(secret_file_host_binary)
|
||||
|
||||
|
||||
# Test text credential
|
||||
t.assertEqual(machine.succeed("systemd-creds --system cat secret").strip(), "foo\nbar\nbaz")
|
||||
|
||||
t.assertEqual(machine.succeed("systemd-creds --system cat secret-default-mechanism").strip(), "default-mechanism")
|
||||
|
||||
# Test credential from nix store
|
||||
t.assertEqual(machine.succeed("systemd-creds --system cat secret-file-nix-store").strip(), "${secret-file}")
|
||||
|
||||
# Test credential from host file
|
||||
t.assertEqual(machine.succeed("systemd-creds --system cat secret-file-host").strip(), secret_file_host)
|
||||
|
||||
# Test binary credential - verify exact binary content
|
||||
result = machine.succeed("systemd-creds --system cat secret-file-host-binary --transcode=base64").strip()
|
||||
expected = base64.b64encode(secret_file_host_binary).decode('ascii')
|
||||
t.assertEqual(result, expected, f"Binary credential mismatch: got {result}, expected {expected}")
|
||||
'';
|
||||
}
|
||||
Reference in New Issue
Block a user