From 7da9ff9fabf19de2306650ad05956ba10f356049 Mon Sep 17 00:00:00 2001 From: Ilan Joselevich Date: Mon, 31 Mar 2025 20:21:23 +0100 Subject: [PATCH] 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 --- nixos/modules/module-list.nix | 1 + nixos/modules/services/security/openbao.nix | 160 ++++++++++++++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/openbao.nix | 105 +++++++++++++ pkgs/by-name/op/openbao/package.nix | 49 +++--- pkgs/by-name/op/openbao/ui.nix | 34 +++++ 6 files changed, 326 insertions(+), 24 deletions(-) create mode 100644 nixos/modules/services/security/openbao.nix create mode 100644 nixos/tests/openbao.nix create mode 100644 pkgs/by-name/op/openbao/ui.nix diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 1cf9cdbfdfe6..616e9e6b3394 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -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 diff --git a/nixos/modules/services/security/openbao.nix b/nixos/modules/services/security/openbao.nix new file mode 100644 index 000000000000..9d75ffbebcb7 --- /dev/null +++ b/nixos/modules/services/security/openbao.nix @@ -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..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"; + }; + }; + }; +} diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index e57f2a1ed0d5..2cada8eaaa0f 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -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 { }; diff --git a/nixos/tests/openbao.nix b/nixos/tests/openbao.nix new file mode 100644 index 000000000000..94e1f0af7ec4 --- /dev/null +++ b/nixos/tests/openbao.nix @@ -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 'OpenBao'") + + 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" + ''; +} diff --git a/pkgs/by-name/op/openbao/package.nix b/pkgs/by-name/op/openbao/package.nix index 7a5ec65ddf07..51737c442f18 100644 --- a/pkgs/by-name/op/openbao/package.nix +++ b/pkgs/by-name/op/openbao/package.nix @@ -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 ]; }; -} +}) diff --git a/pkgs/by-name/op/openbao/ui.nix b/pkgs/by-name/op/openbao/ui.nix new file mode 100644 index 000000000000..b81451b5b8db --- /dev/null +++ b/pkgs/by-name/op/openbao/ui.nix @@ -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; +})