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 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() {
rm -f $docker_compose_yaml
}
@ -203,6 +205,26 @@ do_build() {
}
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;
in
@ -22,6 +27,7 @@ let
./modules/composition/host-environment.nix
./modules/composition/images.nix
./modules/composition/service-info.nix
./modules/composition/text-secrets.nix
];
argsModule = {
@ -30,6 +36,7 @@ let
config._module.args.pkgs = lib.mkIf (pkgs != null) (lib.mkForce pkgs);
config.host.nixStorePrefix = hostNixStorePrefix;
config.host.uid = lib.toInt uid;
config.host.writableStore = writableStore;
};
in

View file

@ -11,8 +11,36 @@
*/
{ pkgs, lib, config, ... }:
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; };
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
{
options = {
@ -42,6 +70,11 @@ in
description = "Attribute set of evaluated service configurations.";
readOnly = true;
};
docker-compose.secrets = lib.mkOption {
type = attrsOf secretType;
description = dockerComposeRef "secrets";
default = {};
};
};
config = {
build.dockerComposeYaml = pkgs.writeText "docker-compose.yaml" config.build.dockerComposeYamlText;
@ -52,6 +85,13 @@ in
version = "3.4";
services = lib.mapAttrs (k: c: c.config.build.service) config.docker-compose.evaluatedServices;
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, ... }:
let
inherit (lib) mkOption types;
inherit (lib) mkOption types mapAttrs mapAttrsToList;
inherit (types) listOf nullOr attrsOf str either int bool;
cfg = config.service;
link = url: text:
''<link xlink:href="${url}">${text}</link>'';
dockerComposeRef = fragment:
@ -23,6 +25,48 @@ let
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);
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
{
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 = {
@ -242,6 +299,8 @@ in
inherit (config.service) restart;
} // lib.optionalAttrs (config.service.stop_signal != null) {
inherit (config.service) stop_signal;
} // lib.optionalAttrs (secrets != null) {
inherit secrets;
} // lib.optionalAttrs (config.service.tmpfs != []) {
inherit (config.service) tmpfs;
} // 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->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