Compare commits
2 commits
main
...
secrets-te
Author | SHA1 | Date | |
---|---|---|---|
|
d42da1a9fb | ||
|
a6878c0391 |
10 changed files with 197 additions and 3 deletions
24
src/arion
24
src/arion
|
@ -118,7 +118,9 @@ fi
|
||||||
debug docker_compose_args: "${docker_compose_args[@]}"
|
debug docker_compose_args: "${docker_compose_args[@]}"
|
||||||
debug files: "${files[@]}"
|
debug files: "${files[@]}"
|
||||||
|
|
||||||
docker_compose_yaml=.tmp-nix-docker-compose-$$-$RANDOM.yaml
|
docker_system_id="$(docker info --format '{{.Name}}-{{.ID}}')"
|
||||||
|
|
||||||
|
docker_compose_yaml=.tmp-arion-$$-$RANDOM.yaml
|
||||||
cleanup() {
|
cleanup() {
|
||||||
rm -f $docker_compose_yaml
|
rm -f $docker_compose_yaml
|
||||||
}
|
}
|
||||||
|
@ -203,6 +205,26 @@ do_build() {
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
)"
|
)"
|
||||||
|
|
||||||
|
# FIXME: Do something else for swarm
|
||||||
|
# FIXME: Include project name
|
||||||
|
export ARION_SECRETS_DIR="arion-secrets/$docker_system_id"
|
||||||
|
|
||||||
|
if [[ true = "$(jq <"$docker_compose_yaml" '.["x-arion"].hasTextSecrets')" ]]; then
|
||||||
|
echo 1>&2 "Evaluating configuration read-only for secrets..."
|
||||||
|
eval "$(nix-instantiate \
|
||||||
|
"$nix_dir/eval-composition.nix" \
|
||||||
|
--eval \
|
||||||
|
--readonly-mode \
|
||||||
|
--json \
|
||||||
|
--argstr uid "$UID" \
|
||||||
|
--arg modules "$modules" \
|
||||||
|
--arg pkgs "$pkgs_argument" \
|
||||||
|
--arg writableStore false \
|
||||||
|
--show-trace \
|
||||||
|
--attr 'config.build.writeSecretsScript' \
|
||||||
|
| jq -r)"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,9 @@
|
||||||
{ modules ? [], uid ? "0", pkgs, hostNixStorePrefix ? "", }:
|
{ modules ? []
|
||||||
|
, uid ? "0"
|
||||||
|
, pkgs
|
||||||
|
, hostNixStorePrefix ? ""
|
||||||
|
, writableStore ? true
|
||||||
|
}:
|
||||||
|
|
||||||
let _pkgs = pkgs;
|
let _pkgs = pkgs;
|
||||||
in
|
in
|
||||||
|
@ -22,6 +27,7 @@ let
|
||||||
./modules/composition/host-environment.nix
|
./modules/composition/host-environment.nix
|
||||||
./modules/composition/images.nix
|
./modules/composition/images.nix
|
||||||
./modules/composition/service-info.nix
|
./modules/composition/service-info.nix
|
||||||
|
./modules/composition/text-secrets.nix
|
||||||
];
|
];
|
||||||
|
|
||||||
argsModule = {
|
argsModule = {
|
||||||
|
@ -30,6 +36,7 @@ let
|
||||||
config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);
|
config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);
|
||||||
config.host.nixStorePrefix = hostNixStorePrefix;
|
config.host.nixStorePrefix = hostNixStorePrefix;
|
||||||
config.host.uid = lib.toInt uid;
|
config.host.uid = lib.toInt uid;
|
||||||
|
config.host.writableStore = writableStore;
|
||||||
};
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
|
|
|
@ -11,8 +11,36 @@
|
||||||
*/
|
*/
|
||||||
{ pkgs, lib, config, ... }:
|
{ pkgs, lib, config, ... }:
|
||||||
let
|
let
|
||||||
|
cfg = config.docker-compose;
|
||||||
|
inherit (lib) mkOption optionalAttrs mapAttrs;
|
||||||
|
inherit (lib.types) submodule attrsOf nullOr either str path bool;
|
||||||
evalService = name: modules: pkgs.callPackage ../../eval-service.nix {} { inherit name modules; inherit (config) host; };
|
evalService = name: modules: pkgs.callPackage ../../eval-service.nix {} { inherit name modules; inherit (config) host; };
|
||||||
|
|
||||||
|
dockerComposeRef = fragment:
|
||||||
|
''See <link xlink:href="https://docs.docker.com/compose/compose-file/#${fragment}">Docker Compose#${fragment}</link>'';
|
||||||
|
|
||||||
|
secretType = submodule {
|
||||||
|
options = {
|
||||||
|
file = mkOption {
|
||||||
|
type = either path str;
|
||||||
|
description = ''
|
||||||
|
Sets the secret's value to this file.
|
||||||
|
|
||||||
|
${dockerComposeRef "secrets"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
external = mkOption {
|
||||||
|
type = bool;
|
||||||
|
default = false;
|
||||||
|
description = ''
|
||||||
|
Whether the value of this secret is set via other means.
|
||||||
|
|
||||||
|
${dockerComposeRef "secrets"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
|
@ -42,6 +70,11 @@ in
|
||||||
description = "Attribute set of evaluated service configurations.";
|
description = "Attribute set of evaluated service configurations.";
|
||||||
readOnly = true;
|
readOnly = true;
|
||||||
};
|
};
|
||||||
|
docker-compose.secrets = lib.mkOption {
|
||||||
|
type = attrsOf secretType;
|
||||||
|
description = dockerComposeRef "secrets";
|
||||||
|
default = {};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
config = {
|
config = {
|
||||||
build.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.build.dockerComposeYamlText;
|
build.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.build.dockerComposeYamlText;
|
||||||
|
@ -52,6 +85,13 @@ in
|
||||||
version = "3.4";
|
version = "3.4";
|
||||||
services = lib.mapAttrs (k: c: c.config.build.service) config.docker-compose.evaluatedServices;
|
services = lib.mapAttrs (k: c: c.config.build.service) config.docker-compose.evaluatedServices;
|
||||||
x-arion = config.docker-compose.extended;
|
x-arion = config.docker-compose.extended;
|
||||||
|
} // optionalAttrs (cfg.secrets != {}) {
|
||||||
|
secrets = mapAttrs (_k: s: optionalAttrs (s.external != false) {
|
||||||
|
inherit (s) external;
|
||||||
|
} // optionalAttrs (s.file != null) {
|
||||||
|
file = toString s.file;
|
||||||
|
}
|
||||||
|
) cfg.secrets;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,5 +29,14 @@
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
host.writableStore = lib.mkOption {
|
||||||
|
type = lib.types.bool;
|
||||||
|
description = ''
|
||||||
|
Whether the Nix store is writable. Normally it is, but when extracting
|
||||||
|
secrets, it must not be writable in order to prevent secrets from
|
||||||
|
accidentally leaking into the Nix store.
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
29
src/nix/modules/composition/text-secrets.nix
Normal file
29
src/nix/modules/composition/text-secrets.nix
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{ config, lib, ... }:
|
||||||
|
let
|
||||||
|
inherit (lib) mkOption mapAttrsToList concatStrings escapeShellArg;
|
||||||
|
inherit (lib.types) attrsOf unspecified;
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
textSecrets = mkOption {
|
||||||
|
type = attrsOf unspecified; # unspecified for laziness
|
||||||
|
default = {};
|
||||||
|
description = "Secrets to write to files.";
|
||||||
|
};
|
||||||
|
build.writeSecretsScript = mkOption {
|
||||||
|
type = unspecified; # unspecified for laziness
|
||||||
|
readOnly = true;
|
||||||
|
internal = true;
|
||||||
|
description = "Generated script that writes the textSecrets.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
docker-compose.extended.hasTextSecrets = config.textSecrets != {};
|
||||||
|
build.writeSecretsScript = concatStrings (mapAttrsToList (k: v: ''
|
||||||
|
mkdir -p "$ARION_SECRETS_DIR"
|
||||||
|
echo ${escapeShellArg v} >$ARION_SECRETS_DIR/${escapeShellArg k}
|
||||||
|
'') config.textSecrets);
|
||||||
|
};
|
||||||
|
}
|
|
@ -7,9 +7,11 @@
|
||||||
{ pkgs, lib, config, ... }:
|
{ pkgs, lib, config, ... }:
|
||||||
|
|
||||||
let
|
let
|
||||||
inherit (lib) mkOption types;
|
inherit (lib) mkOption types mapAttrs mapAttrsToList;
|
||||||
inherit (types) listOf nullOr attrsOf str either int bool;
|
inherit (types) listOf nullOr attrsOf str either int bool;
|
||||||
|
|
||||||
|
cfg = config.service;
|
||||||
|
|
||||||
link = url: text:
|
link = url: text:
|
||||||
''<link xlink:href="${url}">${text}</link>'';
|
''<link xlink:href="${url}">${text}</link>'';
|
||||||
dockerComposeRef = fragment:
|
dockerComposeRef = fragment:
|
||||||
|
@ -23,6 +25,48 @@ let
|
||||||
cap_add = lib.attrNames (lib.filterAttrs (name: value: value == true) config.service.capabilities);
|
cap_add = lib.attrNames (lib.filterAttrs (name: value: value == true) config.service.capabilities);
|
||||||
cap_drop = lib.attrNames (lib.filterAttrs (name: value: value == false) config.service.capabilities);
|
cap_drop = lib.attrNames (lib.filterAttrs (name: value: value == false) config.service.capabilities);
|
||||||
|
|
||||||
|
serviceSecretType = types.submodule {
|
||||||
|
options = {
|
||||||
|
source = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
default = null;
|
||||||
|
description = dockerComposeRef "secrets";
|
||||||
|
};
|
||||||
|
uid = mkOption {
|
||||||
|
type = nullOr (either str int);
|
||||||
|
default = null;
|
||||||
|
description = dockerComposeRef "secrets";
|
||||||
|
};
|
||||||
|
gid = mkOption {
|
||||||
|
type = nullOr (either str int);
|
||||||
|
default = null;
|
||||||
|
description = dockerComposeRef "secrets";
|
||||||
|
};
|
||||||
|
mode = mkOption {
|
||||||
|
type = nullOr str;
|
||||||
|
# default = "0444";
|
||||||
|
default = null;
|
||||||
|
description = ''
|
||||||
|
The default value of is usually 0444. This option may not be supported
|
||||||
|
when not deploying to a Swarm.
|
||||||
|
|
||||||
|
${dockerComposeRef "secrets"}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
secrets = mapAttrsToList (k: s: {
|
||||||
|
target = k;
|
||||||
|
} //lib.optionalAttrs (s.source != null) {
|
||||||
|
inherit (s) source;
|
||||||
|
} // lib.optionalAttrs (s.uid != null) {
|
||||||
|
inherit (s) uid;
|
||||||
|
} // lib.optionalAttrs (s.gid != null) {
|
||||||
|
inherit (s) gid;
|
||||||
|
} // lib.optionalAttrs (s.mode != null) {
|
||||||
|
inherit (s) mode;
|
||||||
|
}) cfg.secrets;
|
||||||
|
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
|
@ -197,6 +241,19 @@ in
|
||||||
}
|
}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
service.secrets = mkOption {
|
||||||
|
type = attrsOf serviceSecretType;
|
||||||
|
default = {};
|
||||||
|
description = dockerComposeRef "secrets";
|
||||||
|
example = {
|
||||||
|
redis_secret = {
|
||||||
|
source = "web_cache_redis_secret";
|
||||||
|
uid = 123;
|
||||||
|
gid = 123;
|
||||||
|
mode = "0440";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config.build.service = {
|
config.build.service = {
|
||||||
|
@ -242,6 +299,8 @@ in
|
||||||
inherit (config.service) restart;
|
inherit (config.service) restart;
|
||||||
} // lib.optionalAttrs (config.service.stop_signal != null) {
|
} // lib.optionalAttrs (config.service.stop_signal != null) {
|
||||||
inherit (config.service) stop_signal;
|
inherit (config.service) stop_signal;
|
||||||
|
} // lib.optionalAttrs (secrets != null) {
|
||||||
|
inherit secrets;
|
||||||
} // lib.optionalAttrs (config.service.tmpfs != []) {
|
} // lib.optionalAttrs (config.service.tmpfs != []) {
|
||||||
inherit (config.service) tmpfs;
|
inherit (config.service) tmpfs;
|
||||||
} // lib.optionalAttrs (config.service.tty != null) {
|
} // lib.optionalAttrs (config.service.tty != null) {
|
||||||
|
|
|
@ -63,5 +63,13 @@ in
|
||||||
$machine->succeed("cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down && rm -rf work");
|
$machine->succeed("cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down && rm -rf work");
|
||||||
$machine->waitUntilFails("curl localhost:8000");
|
$machine->waitUntilFails("curl localhost:8000");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
subtest "secrets", sub {
|
||||||
|
$machine->succeed("cp -r ${../testcases/secrets} work && cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion up -d");
|
||||||
|
$machine->waitUntilSucceeds("curl localhost:8000");
|
||||||
|
$machine->succeed("test qux = \"$(curl localhost:8000)\"");
|
||||||
|
$machine->succeed("cd work && NIX_PATH=nixpkgs='${pkgs.path}' arion down && rm -rf work");
|
||||||
|
$machine->waitUntilFails("curl localhost:8000");
|
||||||
|
};
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|
17
tests/testcases/secrets/arion-compose.nix
Normal file
17
tests/testcases/secrets/arion-compose.nix
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
docker-compose.services.webserver = { pkgs, ... }: {
|
||||||
|
nixos.useSystemd = true;
|
||||||
|
nixos.configuration.boot.tmpOnTmpfs = true;
|
||||||
|
nixos.configuration.services.nginx.enable = true;
|
||||||
|
|
||||||
|
# Please don't do this
|
||||||
|
nixos.configuration.services.nginx.virtualHosts.localhost.root = "/run/secrets";
|
||||||
|
|
||||||
|
service.useHostStore = true;
|
||||||
|
service.ports = [
|
||||||
|
"8000:80" # host:container
|
||||||
|
];
|
||||||
|
service.secrets."foo.txt".source = "foo";
|
||||||
|
};
|
||||||
|
docker-compose.secrets.foo.file = ./foo.key;
|
||||||
|
}
|
2
tests/testcases/secrets/arion-pkgs.nix
Normal file
2
tests/testcases/secrets/arion-pkgs.nix
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# Instead of pinning Nixpkgs, we can opt to use the one in NIX_PATH
|
||||||
|
import <nixpkgs> {}
|
1
tests/testcases/secrets/foo.key
Normal file
1
tests/testcases/secrets/foo.key
Normal file
|
@ -0,0 +1 @@
|
||||||
|
qux
|
Loading…
Reference in a new issue