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

Changed files
+326 -24
nixos
modules
services
security
tests
pkgs
by-name
op
+1
nixos/modules/module-list.nix
···
./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
+160
nixos/modules/services/security/openbao.nix
···
+
{
+
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";
+
};
+
};
+
};
+
}
+1
nixos/tests/all-tests.nix
···
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
···
+
{ 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"
+
'';
+
}
+25 -24
pkgs/by-name/op/openbao/package.nix
···
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=";
};
···
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
];
···
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 ];
};
-
}
+
})
+34
pkgs/by-name/op/openbao/ui.nix
···
+
{
+
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;
+
})