nixos/openbao: init

- Added a NixOS module using RFC42 and plenty of systemd hardening
- Added a NixOS VM Test which checks the basic functionality
- Refactored the package to support HSM and UI
This commit is contained in:
Ilan Joselevich 2025-03-31 20:21:23 +01:00
parent 9d47e86d18
commit 7da9ff9fab
No known key found for this signature in database
6 changed files with 326 additions and 24 deletions

View file

@ -1426,6 +1426,7 @@
./services/security/nginx-sso.nix
./services/security/oauth2-proxy.nix
./services/security/oauth2-proxy-nginx.nix
./services/security/openbao.nix
./services/security/opensnitch.nix
./services/security/paretosecurity.nix
./services/security/pass-secret-service.nix

View file

@ -0,0 +1,160 @@
{
config,
lib,
pkgs,
...
}:
let
cfg = config.services.openbao;
settingsFormat = pkgs.formats.json { };
in
{
options = {
services.openbao = {
enable = lib.mkEnableOption "OpenBao daemon";
package = lib.mkPackageOption pkgs "openbao" {
example = "pkgs.openbao.override { withHsm = false; withUi = false; }";
};
settings = lib.mkOption {
description = ''
Settings of OpenBao.
See [documentation](https://openbao.org/docs/configuration) for more details.
'';
example = lib.literalExpression ''
{
ui = true;
listener.default = {
type = "tcp";
tls_acme_email = config.security.acme.defaults.email;
tls_acme_domains = [ "example.com" ];
tls_acme_disable_http_challenge = true;
};
cluster_addr = "http://127.0.0.1:8201";
api_addr = "https://example.com";
storage.raft.path = "/var/lib/openbao";
}
'';
type = lib.types.submodule {
freeformType = settingsFormat.type;
options = {
ui = lib.mkEnableOption "the OpenBao web UI";
listener = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule (
{ config, ... }:
{
freeformType = settingsFormat.type;
options = {
type = lib.mkOption {
type = lib.types.enum [
"tcp"
"unix"
];
description = ''
The listener type to enable.
'';
};
address = lib.mkOption {
type = lib.types.str;
default = if config.type == "unix" then "/run/openbao/openbao.sock" else "127.0.0.1:8200";
defaultText = lib.literalExpression ''if config.services.openbao.settings.listener.<name>.type == "unix" then "/run/openbao/openbao.sock" else "127.0.0.1:8200"'';
description = ''
The TCP address or UNIX socket path to listen on.
'';
};
};
}
)
);
description = ''
Configure a listener for responding to requests.
'';
};
};
};
};
extraArgs = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ ];
description = ''
Additional arguments given to OpenBao.
'';
};
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [ cfg.package ];
systemd.services.openbao = {
description = "OpenBao - A tool for managing secrets";
wantedBy = [ "multi-user.target" ];
after = [ "network.target" ];
restartIfChanged = false; # do not restart on "nixos-rebuild switch". It would seal the storage and disrupt the clients.
serviceConfig = {
Type = "notify";
ExecStart = lib.escapeShellArgs (
[
(lib.getExe cfg.package)
"server"
"-config"
(settingsFormat.generate "openbao.hcl.json" cfg.settings)
]
++ cfg.extraArgs
);
ExecReload = "${lib.getExe' pkgs.coreutils "kill"} -SIGHUP $MAINPID";
StateDirectory = "openbao";
StateDirectoryMode = "0700";
RuntimeDirectory = "openbao";
RuntimeDirectoryMode = "0700";
CapabilityBoundingSet = "";
DynamicUser = true;
LimitCORE = 0;
LockPersonality = true;
MemorySwapMax = 0;
MemoryZSwapMax = 0;
PrivateUsers = true;
ProcSubset = "pid";
ProtectClock = true;
ProtectControlGroups = true;
ProtectHome = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectProc = "invisible";
Restart = "on-failure";
RestrictAddressFamilies = [
"AF_INET"
"AF_INET6"
"AF_UNIX"
];
RestrictNamespaces = true;
RestrictRealtime = true;
SystemCallArchitectures = "native";
SystemCallFilter = [
"@system-service"
"@resources"
"~@privileged"
];
UMask = "0077";
};
};
};
}

View file

@ -972,6 +972,7 @@ in
ollama-rocm = runTestOn [ "x86_64-linux" "aarch64-linux" ] ./ollama-rocm.nix;
ombi = handleTest ./ombi.nix { };
openarena = handleTest ./openarena.nix { };
openbao = runTest ./openbao.nix;
openldap = handleTest ./openldap.nix { };
opensearch = discoverTests (import ./opensearch.nix);
openresty-lua = handleTest ./openresty-lua.nix { };

105
nixos/tests/openbao.nix Normal file
View file

@ -0,0 +1,105 @@
{ lib, ... }:
let
certs = import ./common/acme/server/snakeoil-certs.nix;
domain = certs.domain;
in
{
name = "openbao";
meta.maintainers = with lib.maintainers; [ kranzes ];
nodes.machine =
{ config, ... }:
{
security.pki.certificateFiles = [ certs.ca.cert ];
networking.extraHosts = ''
127.0.0.1 ${domain}
'';
services.openbao = {
enable = true;
settings = {
ui = true;
listener = {
default = {
type = "tcp";
tls_cert_file = certs.${domain}.cert;
tls_key_file = certs.${domain}.key;
};
unix = {
type = "unix";
};
};
cluster_addr = "https://127.0.0.1:8201";
api_addr = "https://${domain}:8200";
storage.raft.path = "/var/lib/openbao";
};
};
environment.variables = {
BAO_ADDR = config.services.openbao.settings.api_addr;
BAO_FORMAT = "json";
};
};
testScript =
{ nodes, ... }:
''
import json
start_all()
with subtest("Wait for OpenBao to start up"):
machine.wait_for_unit("openbao.service")
machine.wait_for_open_port(8200)
machine.wait_for_open_unix_socket("${nodes.machine.services.openbao.settings.listener.unix.address}")
with subtest("Check that the web UI is being served"):
machine.succeed("curl -L --fail --show-error --silent $BAO_ADDR | grep '<title>OpenBao</title>'")
with subtest("Check that OpenBao is not initialized"):
status_output = json.loads(machine.fail("bao status"))
assert not status_output["initialized"]
with subtest("Initialize OpenBao"):
init_output = json.loads(machine.succeed("bao operator init"))
with subtest("Check that OpenBao is initialized and sealed"):
status_output = json.loads(machine.fail("bao status"))
assert status_output["initialized"]
assert status_output["sealed"]
with subtest("Unseal OpenBao"):
for key in init_output["unseal_keys_b64"][:init_output["unseal_threshold"]]:
machine.succeed(f"bao operator unseal {key}")
with subtest("Check that OpenBao is not sealed"):
status_output = json.loads(machine.succeed("bao status"))
assert not status_output["sealed"]
with subtest("Login with root token"):
machine.succeed(f"bao login {init_output["root_token"]}")
with subtest("Enable userpass auth method"):
machine.succeed("bao auth enable userpass")
with subtest("Create a user in userpass"):
machine.succeed("bao write auth/userpass/users/testuser password=testpassword")
with subtest("Login to a user from userpass"):
machine.succeed("bao login -method userpass username=testuser password=testpassword")
with subtest("Write a secret to cubbyhole"):
machine.succeed("bao write cubbyhole/my-secret my-value=s3cr3t")
with subtest("Read a secret from cubbyhole"):
read_output = json.loads(machine.succeed("bao read cubbyhole/my-secret"))
assert read_output["data"]["my-value"] == "s3cr3t"
'';
}

View file

@ -3,20 +3,23 @@
fetchFromGitHub,
buildGoModule,
go_1_24,
testers,
openbao,
versionCheckHook,
nix-update-script,
nixosTests,
callPackage,
stdenvNoCC,
withUi ? true,
withHsm ? stdenvNoCC.hostPlatform.isLinux,
}:
buildGoModule.override { go = go_1_24; } rec {
buildGoModule.override { go = go_1_24; } (finalAttrs: {
pname = "openbao";
version = "2.2.0";
src = fetchFromGitHub {
owner = "openbao";
repo = "openbao";
tag = "v${version}";
tag = "v${finalAttrs.version}";
hash = "sha256-dDMOeAceMaSrF7P4JZ2MKy6zDa10LxCQKkKwu/Q3kOU=";
};
@ -26,33 +29,24 @@ buildGoModule.override { go = go_1_24; } rec {
subPackages = [ "." ];
tags = [
"openbao"
"bao"
];
tags = lib.optional withHsm "hsm" ++ lib.optional withUi "ui";
ldflags = [
"-s"
"-w"
"-X github.com/openbao/openbao/version.GitCommit=${src.rev}"
"-X github.com/openbao/openbao/version.fullVersion=${version}"
"-X github.com/openbao/openbao/version.GitCommit=${finalAttrs.src.rev}"
"-X github.com/openbao/openbao/version.fullVersion=${finalAttrs.version}"
"-X github.com/openbao/openbao/version.buildDate=1970-01-01T00:00:00Z"
];
postConfigure = lib.optionalString withUi ''
cp -r --no-preserve=mode ${finalAttrs.passthru.ui} http/web_ui
'';
postInstall = ''
mv $out/bin/openbao $out/bin/bao
'';
# TODO: Enable the NixOS tests after adding OpenBao as a NixOS service in an upcoming PR and
# adding NixOS tests
#
# passthru.tests = { inherit (nixosTests) vault vault-postgresql vault-dev vault-agent; };
passthru.tests.version = testers.testVersion {
package = openbao;
command = "HOME=$(mktemp -d) bao --version";
version = "v${version}";
};
nativeInstallCheckInputs = [
versionCheckHook
];
@ -61,15 +55,22 @@ buildGoModule.override { go = go_1_24; } rec {
doInstallCheck = true;
passthru = {
updateScript = nix-update-script { };
ui = callPackage ./ui.nix { };
tests = { inherit (nixosTests) openbao; };
updateScript = nix-update-script {
extraArgs = [
"--subpackage"
"ui"
];
};
};
meta = {
homepage = "https://www.openbao.org/";
description = "Open source, community-driven fork of Vault managed by the Linux Foundation";
changelog = "https://github.com/openbao/openbao/blob/v${version}/CHANGELOG.md";
changelog = "https://github.com/openbao/openbao/blob/v${finalAttrs.version}/CHANGELOG.md";
license = lib.licenses.mpl20;
mainProgram = "bao";
maintainers = with lib.maintainers; [ brianmay ];
};
}
})

View file

@ -0,0 +1,34 @@
{
stdenvNoCC,
openbao,
yarn-berry_3,
nodejs,
}:
stdenvNoCC.mkDerivation (finalAttrs: {
pname = openbao.pname + "-ui";
inherit (openbao) version src;
sourceRoot = "${finalAttrs.src.name}/ui";
offlineCache = yarn-berry_3.fetchYarnBerryDeps {
inherit (finalAttrs) src sourceRoot;
hash = "sha256-bQ+ph7CvPtygvCoCMjTMadYLn/ds2ZOGQL29x3hFuLg=";
};
nativeBuildInputs = [
yarn-berry_3.yarnBerryConfigHook
nodejs
yarn-berry_3
];
env.YARN_ENABLE_SCRIPTS = 0;
postConfigure = ''
substituteInPlace .ember-cli \
--replace-fail "../http/web_ui" "$out"
'';
buildPhase = "yarn run ember build --environment=production";
dontInstall = true;
})