WIP: secret management at scale #150
|
@ -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 = [
|
||||||
|
|
|
@ -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 = ''
|
||||||
|
|
|
@ -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" ];
|
||||||
|
|
||||||
|
|
|
@ -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
21
services/self/default.nix
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
65
services/vault/default.nix
Normal file
65
services/vault/default.nix
Normal 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
237
services/vault/module.nix
Normal 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
|
|||||||
|
# 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
51
services/vault/package.nix
Normal file
51
services/vault/package.nix
Normal 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 ];
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue
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.
@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 :)