WIP: secret management at scale #150

Draft
raito wants to merge 2 commits from vault into main
8 changed files with 400 additions and 2 deletions

View file

@ -1,7 +1,7 @@
{ lib, config, ... }: { lib, config, ... }:
let let
cfg = config.bagel.hardware.raito-vm; cfg = config.bagel.hardware.raito-vm;
inherit (lib) mkEnableOption mkIf mkOption types; inherit (lib) mkEnableOption mkIf mkOption types split toIntBase10;
in in
{ {
options.bagel.hardware.raito-vm = { options.bagel.hardware.raito-vm = {
@ -54,6 +54,17 @@ in
linkConfig.Name = "wan"; linkConfig.Name = "wan";
}; };
bagel.infra.self.wan =
let
parts = split "/" cfg.networking.wan.address;
address = builtins.elemAt parts 0;
prefixLength = toIntBase10 (builtins.elemAt 1 parts);
in
{
family = "inet6";
inherit address prefixLength;
};
boot.loader.systemd-boot.enable = true; boot.loader.systemd-boot.enable = true;
boot.initrd.kernelModules = [ boot.initrd.kernelModules = [

View file

@ -72,6 +72,12 @@
]; ];
networking.defaultGateway6 = { interface = "uplink"; address = "2a01:584:11::1"; }; networking.defaultGateway6 = { interface = "uplink"; address = "2a01:584:11::1"; };
bagel.infra.self.wan = {
family = "inet6";
address = "2a01:584:11::1:11";
prefixLength = 64;
};
services.coredns = { services.coredns = {
enable = true; enable = true;
config = '' config = ''

View file

@ -135,6 +135,11 @@ in
{ address = "2a01:584:11::1:${toString cfg.num}"; prefixLength = 64; } { address = "2a01:584:11::1:${toString cfg.num}"; prefixLength = 64; }
]; ];
networking.defaultGateway6 = { interface = "uplink"; address = "2a01:584:11::1"; }; networking.defaultGateway6 = { interface = "uplink"; address = "2a01:584:11::1"; };
bagel.infra.self.wan = {
family = "inet6";
address = "2a01:584:11::1:${toString cfg.num}";
prefixLength = 64;
};
deployment.targetHost = "2a01:584:11::1:${toString cfg.num}"; deployment.targetHost = "2a01:584:11::1:${toString cfg.num}";
deployment.tags = [ "builders" ]; deployment.tags = [ "builders" ];

View file

@ -7,6 +7,7 @@
./matrix ./matrix
./monitoring ./monitoring
./uptime-kuma ./uptime-kuma
./self
./netbox ./netbox
./ofborg ./ofborg
./postgres ./postgres
@ -15,6 +16,7 @@
./buildbot ./buildbot
./newsletter ./newsletter
./s3-revproxy ./s3-revproxy
./vault
./extra-builders ./extra-builders
]; ];
} }

21
services/self/default.nix Normal file
View file

@ -0,0 +1,21 @@
# This is a data-only module for other modules consumption.
{ lib, ... }:
let
inherit (lib) mkOption types;
in
{
options.bagel.infra.self = {
wan = {
family = mkOption {
type = types.enum [ "inet" "inet6" ];
default = "inet6";
};
address = mkOption {
type = types.str;
};
prefixLength = mkOption {
type = types.int;
};
};
};
}

View file

@ -0,0 +1,65 @@
{ config, lib, ... }:
let
cfg = config.bagel.infra.vault;
inherit (lib) mkEnableOption mkOption mkIf concatStringsSep types;
mkPeerNode = fqdn: ''
retry_join {
leader_api_addr = "https://${fqdn}"
leader_tls_servername = "${fqdn}"
}
'';
wanAddress = if config.bagel.infra.self.wan.family == "inet6" then "[${config.bagel.infra.self.wan.address}]" else "${config.bagel.infra.self.wan.address}";
in
{
options.bagel.infra.vault = {
enable = mkEnableOption "the OpenBao (Vault fork) service";
peers = mkOption {
type = types.listOf types.str;
description = "List of FQDN that are peers of this service";
};
};
imports = [
./module.nix
];
config = mkIf cfg.enable {
networking.firewall.allowedTCPPorts = [
# NGINX HTTP API access
80
443
# mTLS backed cluster port
8201
];
services.nginx = {
enable = true;
recommendedProxySettings = true;
virtualHosts."${config.networking.fqdn}" = {
enableACME = true;
forceSSL = true;
locations."/" = {
proxyPass = "http://127.0.0.1:8200";
};
};
};
services.openbao = {
enable = true;
storageBackend = "raft";
listenerExtraConfig = ''
cluster_address = "${wanAddress}:8201"
'';
storageConfig = ''
node_id = "${config.networking.fqdn}"
# Other nodes of the cluster.
${concatStringsSep "\n" (map mkPeerNode cfg.peers)}
'';
extraConfig = ''
cluster_addr = "http://${config.networking.fqdn}:8201"
api_addr = "https://${config.networking.fqdn}"
'';
};
};
}

237
services/vault/module.nix Normal file
View file

@ -0,0 +1,237 @@
{ config, lib, options, pkgs, ... }:
with lib;
let
cfg = config.services.openbao;
opt = options.services.openbao;
configFile = pkgs.writeText "openbao.hcl" ''
raito marked this conversation as resolved
Review

Every .hcl can be represented as json.
Given this is a new module and not constrained by what the current NixOS services.vault module does, please convert this to be a freeform json attribute.

If you need help with the specific details or simply want me to do it, let me know.
But I am convinced we should do multiline hcl templating here.

Every `.hcl` can be represented as json. Given this is a new module and not constrained by what the current NixOS `services.vault` module does, please convert this to be a freeform json attribute. If you need help with the specific details or simply want me to do it, let me know. But I am convinced we should do multiline hcl templating here.
Review

@emilylange I would love if you could just do it for me :<, that'd be awesome! Feel free to propose a new version of the module BTW :)

@emilylange I would love if you could just do it for me :<, that'd be awesome! Feel free to propose a new version of the module BTW :)
# vault in dev mode will refuse to start if its configuration sets listener
${lib.optionalString (!cfg.dev) ''
listener "tcp" {
address = "${cfg.address}"
${if (cfg.tlsCertFile == null || cfg.tlsKeyFile == null) then ''
tls_disable = "true"
'' else ''
tls_cert_file = "${cfg.tlsCertFile}"
tls_key_file = "${cfg.tlsKeyFile}"
''}
${cfg.listenerExtraConfig}
}
''}
storage "${cfg.storageBackend}" {
${optionalString (cfg.storagePath != null) ''path = "${cfg.storagePath}"''}
${optionalString (cfg.storageConfig != null) cfg.storageConfig}
}
${optionalString (cfg.telemetryConfig != "") ''
telemetry {
${cfg.telemetryConfig}
}
''}
${cfg.extraConfig}
'';
allConfigPaths = [configFile] ++ cfg.extraSettingsPaths;
configOptions = escapeShellArgs
(lib.optional cfg.dev "-dev" ++
lib.optional (cfg.dev && cfg.devRootTokenID != null) "-dev-root-token-id=${cfg.devRootTokenID}"
++ (concatMap (p: ["-config" p]) allConfigPaths));
in
{
options = {
services.openbao = {
enable = mkEnableOption "OpenBao daemon";
package = mkPackageOption pkgs "openbao" { };
dev = mkOption {
type = types.bool;
default = false;
description = ''
In this mode, the Vault runs in-memory and starts unsealed. This option is not meant production but for development and testing i.e. for nixos tests.
'';
};
devRootTokenID = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Initial root token. This only applies when {option}`services.vault.dev` is true
'';
};
address = mkOption {
type = types.str;
default = "127.0.0.1:8200";
description = "The name of the ip interface to listen to";
};
tlsCertFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/path/to/your/cert.pem";
description = "TLS certificate file. TLS will be disabled unless this option is set";
};
tlsKeyFile = mkOption {
type = types.nullOr types.str;
default = null;
example = "/path/to/your/key.pem";
description = "TLS private key file. TLS will be disabled unless this option is set";
};
listenerExtraConfig = mkOption {
type = types.lines;
default = ''
tls_min_version = "tls12"
'';
description = "Extra text appended to the listener section.";
};
storageBackend = mkOption {
type = types.enum [ "inmem" "file" "consul" "zookeeper" "s3" "azure" "dynamodb" "etcd" "mssql" "mysql" "postgresql" "swift" "gcs" "raft" ];
default = "inmem";
description = "The name of the type of storage backend";
};
storagePath = mkOption {
type = types.nullOr types.path;
default = if cfg.storageBackend == "file" || cfg.storageBackend == "raft" then "/var/lib/vault" else null;
defaultText = literalExpression ''
if config.${opt.storageBackend} == "file" || cfg.storageBackend == "raft"
then "/var/lib/vault"
else null
'';
description = "Data directory for file backend";
};
storageConfig = mkOption {
type = types.nullOr types.lines;
default = null;
description = ''
HCL configuration to insert in the storageBackend section.
Confidential values should not be specified here because this option's
value is written to the Nix store, which is publicly readable.
Provide credentials and such in a separate file using
[](#opt-services.vault.extraSettingsPaths).
'';
};
telemetryConfig = mkOption {
type = types.lines;
default = "";
description = "Telemetry configuration";
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Extra text appended to {file}`vault.hcl`.";
};
extraSettingsPaths = mkOption {
type = types.listOf types.path;
default = [];
description = ''
Configuration files to load besides the immutable one defined by the NixOS module.
This can be used to avoid putting credentials in the Nix store, which can be read by any user.
Each path can point to a JSON- or HCL-formatted file, or a directory
to be scanned for files with `.hcl` or
`.json` extensions.
To upload the confidential file with NixOps, use for example:
```
# https://releases.nixos.org/nixops/latest/manual/manual.html#opt-deployment.keys
deployment.keys."vault.hcl" = let db = import ./db-credentials.nix; in {
text = ${"''"}
storage "postgresql" {
connection_url = "postgres://''${db.username}:''${db.password}@host.example.com/exampledb?sslmode=verify-ca"
}
${"''"};
user = "vault";
};
services.vault.extraSettingsPaths = ["/run/keys/vault.hcl"];
services.vault.storageBackend = "postgresql";
users.users.vault.extraGroups = ["keys"];
```
'';
};
};
};
config = mkIf cfg.enable {
nixpkgs.overlays = [ (self: super: {
openbao = super.callPackage ./package.nix { };
}) ];
environment.systemPackages = [
pkgs.openbao
];
assertions = [
{
assertion = cfg.storageBackend == "inmem" -> (cfg.storagePath == null && cfg.storageConfig == null);
message = ''The "inmem" storage expects no services.vault.storagePath nor services.vault.storageConfig'';
}
{
assertion = (
(cfg.storageBackend == "file" -> (cfg.storagePath != null && cfg.storageConfig == null)) &&
(cfg.storagePath != null -> (cfg.storageBackend == "file" || cfg.storageBackend == "raft"))
);
message = ''You must set services.vault.storagePath only when using the "file" or "raft" backend'';
}
];
users.users.openbao = {
name = "openbao";
group = "openbao";
uid = config.ids.uids.vault;
description = "OpenBao daemon user";
};
users.groups.openbao.gid = config.ids.gids.vault;
systemd.tmpfiles.rules = optional (cfg.storagePath != null)
"d '${cfg.storagePath}' 0700 openbao openbao - -";
systemd.services.openbao = {
description = "OpenBao server daemon";
wantedBy = ["multi-user.target"];
after = [ "network.target" ]
++ optional (config.services.consul.enable && cfg.storageBackend == "consul") "consul.service";
restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
startLimitIntervalSec = 60;
startLimitBurst = 3;
serviceConfig = {
User = "openbao";
Group = "openbao";
ExecStart = "${lib.getExe cfg.package} server ${configOptions}";
ExecReload = "${pkgs.coreutils}/bin/kill -SIGHUP $MAINPID";
StateDirectory = "vault";
# In `dev` mode vault will put its token here
Environment = lib.optional (cfg.dev) "HOME=/var/lib/vault";
PrivateDevices = true;
PrivateTmp = true;
ProtectSystem = "full";
ProtectHome = "read-only";
AmbientCapabilities = "cap_ipc_lock";
NoNewPrivileges = true;
LimitCORE = 0;
KillSignal = "SIGINT";
TimeoutStopSec = "30s";
Restart = "on-failure";
};
unitConfig.RequiresMountsFor = optional (cfg.storagePath != null) cfg.storagePath;
};
};
}

View file

@ -0,0 +1,51 @@
{ stdenv, lib, fetchFromGitHub, buildGoModule, installShellFiles, nixosTests
, makeWrapper
, gawk
, glibc
}:
buildGoModule rec {
pname = "openbao";
version = "2.0.2";
src = fetchFromGitHub {
owner = "openbao";
repo = "openbao";
rev = "v${version}";
hash = "sha256-7Dqrw00wjI/VCahY1+ANBMq9nPUQlb94HiBB3CKyhSQ=";
};
vendorHash = "sha256-qojDPhdCqnYCAFo5sc9mWyQxvHc/p/a1LYdW7MbOO5w=";
subPackages = [ "." ];
nativeBuildInputs = [ installShellFiles makeWrapper ];
tags = [ "openbao" ];
ldflags = [
"-s" "-w"
"-X github.com/openbao/openbao/sdk/version.GitCommit=${src.rev}"
"-X github.com/openbao/openbao/sdk/version.Version=${version}"
"-X github.com/openbao/openbao/sdk/version.VersionPrerelease="
];
postInstall = ''
echo "complete -C $out/bin/openbao openbao" > openbao.bash
installShellCompletion openbao.bash
'' + lib.optionalString stdenv.isLinux ''
wrapProgram $out/bin/openbao \
--prefix PATH ${lib.makeBinPath [ gawk glibc ]}
'';
# passthru.tests = { inherit (nixosTests) vault vault-postgresql vault-dev vault-agent; };
meta = with lib; {
homepage = "https://openbao.org/";
description = "Tool for managing secrets";
changelog = "https://github.com/openbao/openbao/blob/v${version}/CHANGELOG.md";
license = licenses.mpl20;
mainProgram = "openbao";
maintainers = with maintainers; [ raitobezarius ];
};
}