Compare commits

...

2 commits

Author SHA1 Message Date
Robert Hensing
d42da1a9fb WIP textual secrets 2019-05-22 14:43:47 +02:00
Robert Hensing
a6878c0391 Add secrets options to service and composition 2019-05-22 14:43:06 +02:00
10 changed files with 197 additions and 3 deletions

View file

@ -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
} }

View file

@ -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

View file

@ -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;
}; };
}; };
} }

View file

@ -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.
'';
};
}; };
} }

View 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);
};
}

View file

@ -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) {

View file

@ -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");
};
''; '';
} }

View 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;
}

View file

@ -0,0 +1,2 @@
# Instead of pinning Nixpkgs, we can opt to use the one in NIX_PATH
import <nixpkgs> {}

View file

@ -0,0 +1 @@
qux