Conversation
20ac2ce to
e4e4b36
Compare
fcd3bba to
beea285
Compare
hatch01
left a comment
There was a problem hiding this comment.
Works in production for a week.
|
Been working on production for half a year now. |
| PrivateUsers = true; | ||
| ProtectClock = true; | ||
| ProtectControlGroups = true; | ||
| ProtectHome = true; |
There was a problem hiding this comment.
Looking at the pictures from the project readme, it looks like the service has a feature for monitoring disk usage. Could it be that the agent (not the hub) won't get access to anything under /home with it marked as private, and therefore reports disk usage incorrectly? I imagine it will still get the overall disk usage correct, just not for /home and other blocked off paths maybe?
|
@h7x4 I can't comment on the last review (not sure why?), so I will do it here: https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#ProtectHome= |
This would still lead to the service reporting 0 disk usage for the home directories, right? The
Upstream seems to suggest that this is the intended way to do it |
|
I implemented the changes for the agent (which I can also test to a certain degree) to take a bit of the burden off of @BonusPlay. @h7x4, regarding the Final beszel-agent.nix{
lib,
config,
pkgs,
...
}:
let
cfg = config.services.beszel.agent;
in
{
meta.maintainers = with lib.maintainers; [
BonusPlay
arunoruto
];
options.services.beszel.agent = {
enable = lib.mkEnableOption "beszel agent";
package = lib.mkPackageOption pkgs "beszel" { };
openFirewall = (lib.mkEnableOption "") // {
description = "Whether to open the firewall port (default 45876).";
};
environment = lib.mkOption {
type = lib.types.attrsOf lib.types.str;
default = { };
description = ''
Environment variables for configuring the beszel-agent service.
This field will end up public in /nix/store, for secret values use `environmentFile`.
'';
};
environmentFile = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
File path containing environment variables for configuring the beszel-agent service in the format of an EnvironmentFile. See {manpage}`systemd.exec(5)`.
'';
};
extraPath = lib.mkOption {
type = lib.types.listOf lib.types.package;
default = [ ];
description = ''
Extra packages to add to beszel path (such as nvidia-smi or rocm-smi).
'';
};
};
config = lib.mkIf cfg.enable {
systemd.services.beszel-agent = {
description = "Beszel Server Monitoring Agent";
wantedBy = [ "multi-user.target" ];
wants = [ "network-online.target" ];
after = [ "network-online.target" ];
environment = cfg.environment;
path =
cfg.extraPath
++ lib.optionals (builtins.elem "nvidia" config.services.xserver.videoDrivers) [
(lib.getBin config.hardware.nvidia.package)
]
++ lib.optionals (builtins.elem "amdgpu" config.services.xserver.videoDrivers) [
(lib.getBin pkgs.rocmPackages.rocm-smi)
]
++ lib.optionals (builtins.elem "intel" config.services.xserver.videoDrivers) [
(lib.getBin pkgs.intel-gpu-tools)
];
serviceConfig = {
ExecStart = ''
${cfg.package}/bin/beszel-agent
'';
EnvironmentFile = cfg.environmentFile;
# adds ability to monitor docker/podman containers
SupplementaryGroups =
lib.optionals config.virtualisation.docker.enable [ "docker" ]
++ lib.optionals (
config.virtualisation.podman.enable && config.virtualisation.podman.dockerSocket.enable
) [ "podman" ];
DynamicUser = true;
User = "beszel-agent";
LockPersonality = true;
NoNewPrivileges = true;
PrivateTmp = true;
PrivateUsers = true;
ProtectClock = true;
ProtectControlGroups = "strict";
ProtectHome = "read-only";
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectSystem = "strict";
Restart = "on-failure";
RestartSec = "30s";
RestrictRealtime = true;
RestrictSUIDSGID = true;
SystemCallArchitectures = "native";
SystemCallErrorNumber = "EPERM";
SystemCallFilter = [ "@system-service" ];
Type = "simple";
UMask = 27;
};
};
networking.firewall.allowedTCPPorts = lib.mkIf cfg.openFirewall [
(
if (builtins.hasAttr "PORT" cfg.environment) then
(lib.strings.toInt cfg.environment.PORT)
else
45876
)
];
};
}Diff: 19c19,21
< openFirewall = lib.mkEnableOption "Open the firewall port (default 45876).";
---
> openFirewall = (lib.mkEnableOption "") // {
> description = "Whether to open the firewall port (default 45876).";
> };
22c24
< type = lib.types.attrs;
---
> type = lib.types.attrsOf lib.types.str;
47c49
< description = "Beszel Agent";
---
> description = "Beszel Server Monitoring Agent";
80a83
> User = "beszel-agent";
86,87c89,90
< ProtectControlGroups = true;
< ProtectHome = true;
---
> ProtectControlGroups = "strict";
> ProtectHome = "read-only";I can do the same for the hub, but someone else would need to test it! |
Correct, only the agent needs access AFAIU. The hub gets the data from one or more agents and is only responsible for handling that info - not collect any more data. So it shouldn't need access to the home directories.
👍 |
| }; | ||
|
|
||
| config = lib.mkIf cfg.enable { | ||
| systemd.services.beszel-agent = { |
There was a problem hiding this comment.
A lot of the setup scripts and docs really seems to suggest that the agent wants a KEY/KEY_FILE of some sort. Since you've been dogfooding this for a while, I assume you set this in the environment file maybe?
Do you think it's important enough to warrant a separate option? Or at least a piece of documentation or description telling downstream module users to set it?
There was a problem hiding this comment.
Yes, it needs to be set (and is pretty much the only important config). However, it is technically a secret value, so KEY should be passed via here.
There was a problem hiding this comment.
I'm not entirely sure what you mean by "here", are you referring to the environment file? In any case, I really think it would be nice to communicate to the module user in some way that this piece of config really needs to be set. Either via a separate unset option keyFile that just sets the environment variable for them and makes the module system force them to configure it, via some piece of a description where the user is likely to see the notice, or via a meta.doc file.
There was a problem hiding this comment.
Yeah, most of the config options are done through the environment variables. I just read-up on how to set it up and came with the following snipper:
environment = lib.mkOption {
type = lib.types.submodule {
freeformType = with lib.types; attrsOf anything;
options = {
EXTRA_FILESYSTEMS = lib.mkOption {
type = lib.types.str;
default = "";
example = ''
lib.strings.concatStringsSep "," [
"sdb"
"sdc1"
"mmcblk0"
"/mnt/network-share"
];
'';
description = ''
Comma separated list of additional filesystems to monitor extra disks.
See <https://beszel.dev/guide/additional-disks>.
'';
};
KEY = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
Public SSH key(s) to use for authentication. Provided in the hub.
'';
};
KEY_FILE = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
Public SSH key(s) from a file instead of an environment variable. Provided in the hub.
'';
};
TOKEN = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
description = ''
WebSocket registration token. Provided in the hub.
'';
};
TOKEN_FILE = lib.mkOption {
type = lib.types.nullOr lib.types.path;
default = null;
description = ''
WebSocket token from a file instead of an environment variable. Provided in the hub.
'';
};
};
};
default = { };
description = ''
Environment variables for configuring the beszel-agent service.
This field will end up public in /nix/store, for secret values use `environmentFile`.
'';
};There was a problem hiding this comment.
I think even having TOKEN and KEY options is a anti-pattern. We shouldn't encourage users to store secrets using plaintext in nix-store. If they do want, the can do that (via environment option).
We could add separate KEY_FILE and TOKEN_FILE options. wdyt?
There was a problem hiding this comment.
Probably no need to expose EXTRA_FILESYSTEMS unless we have a sane default for it or expect most users to want to set it.
There was a problem hiding this comment.
On a second note, any chance the key could be provisioned programmatically somehow? Or is it one of those things where you just have to use the GUI? It would be kinda nice to have a nixos test for all of this...
Edit: found https://github.com/henrygd/beszel/blob/1e32d136506f44f191164c857f730b675adb3812/internal/hub/hub.go#L271 and https://github.com/henrygd/beszel/blob/1e32d136506f44f191164c857f730b675adb3812/internal/hub/hub.go#L224-L227, I'll see if I can cook up something (unless anyone else wants to have a go at it)
There was a problem hiding this comment.
It's an ssh pubkey that is visible from hub's web-ui.
There was a problem hiding this comment.
Probably no need to expose
EXTRA_FILESYSTEMSunless we have a sane default for it or expect most users to want to set it.
So just the two _FILE options
On a second note, any chance the key could be provisioned programmatically somehow? Or is it one of those things where you just have to use the GUI? It would be kinda nice to have a nixos test for all of this...
There is a way to do so via the CLI:
Usage: beszel-agent [command] [flags]
Commands:
health Check if the agent is running
help Display this help message
update Update to the latest version
version Display the version
Flags:
-key string
Public key(s) for SSH authentication
-listen string
Address or port to listen on
But I am not sure if it is feasible.
There was a problem hiding this comment.
That would be where you give the agent the key from the hub, but I was looking for where to get the key from the hub in the first place. Think I found it though :)
68cd4dd to
399ada7
Compare
| RuntimeDirectory = baseNameOf cfg.dataDir; | ||
| ReadWritePaths = cfg.dataDir; | ||
|
|
||
| DynamicUser = true; |
There was a problem hiding this comment.
I am wondering if it would make sense to have this actually have a real system user. When running beszel-hub superuser create ..., the command just assumes that it's going to run as it is in cwd, and creates a new database right there. With a real user, the sysadmin could at least do sudo -u beszel-hub beszel-hub --dir /var/lib/beszel-hub/beszel_data superuser create ...
The alternative would be to put some sort of wrapper script in the environment that runs the commands as the service. There is a fancy new feature in systemd v258 systemd-analyze unit-shell that we might be able to use in a script that would run the commands with the environment of the unit. Using this tool would probably be quite experimental (for better or for worse). Both nsenter and sudo -u could be alternatives for the script, but I don't think they would work while the service is shut down. nsenter inherently so, and sudo -u because the user doesn't exist.
(See also nextcloud)
nixpkgs/nixos/modules/services/web-apps/nextcloud.nix
Lines 127 to 190 in 52077cd
There was a problem hiding this comment.
While we're at it, did you create your superuser through the cli tool, or is there some other way around?
|
I know we can polish this module to perfection and bikeshed for another half a year, but this PR has been in decently-working state for over 8 months. I know it could be better, but I'd like to try get this module merged into 25.11 (we've already missed 25.05). @h7x4 is that possible? After we get it merged I'm sure both me and @arunoruto plan to maintain it. |
Yes, I'd be happy to get it merged by 25.11. It seems like most technical and UX issues with potential for backwards incompatibilities has been ironed out. It took a little while, but I finished a nixos test. Do you mind if I push it on top of the current patchset? EDIT: I see you've disabled maintainers pushing commits, here's the patch: From 0bfad254ceb474554c9e4dea6d6a01937010e3cf Mon Sep 17 00:00:00 2001
From: h7x4 <h7x4@nani.wtf>
Date: Mon, 13 Oct 2025 12:52:20 +0900
Subject: [PATCH] nixos/tests/beszel: init
---
nixos/tests/all-tests.nix | 1 +
nixos/tests/beszel.nix | 119 +++++++++++++++++++++++++++++
pkgs/by-name/be/beszel/package.nix | 6 +-
3 files changed, 125 insertions(+), 1 deletion(-)
create mode 100644 nixos/tests/beszel.nix
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index e471baaf7849..c879ec56938b 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -260,6 +260,7 @@ in
beanstalkd = runTest ./beanstalkd.nix;
bees = runTest ./bees.nix;
benchexec = runTest ./benchexec.nix;
+ beszel = runTest ./beszel.nix;
binary-cache = runTest {
imports = [ ./binary-cache.nix ];
_module.args.compression = "zstd";
diff --git a/nixos/tests/beszel.nix b/nixos/tests/beszel.nix
new file mode 100644
index 000000000000..77a4a32a3747
--- /dev/null
+++ b/nixos/tests/beszel.nix
@@ -0,0 +1,119 @@
+{ pkgs, lib, ... }:
+{
+ name = "beszel";
+ meta.maintainers = with lib.maintainers; [ h7x4 ];
+
+ nodes = {
+ hubHost =
+ { config, pkgs, ... }:
+ {
+ virtualisation.vlans = [ 1 ];
+
+ systemd.network.networks."01-eth1" = {
+ name = "eth1";
+ networkConfig.Address = "10.0.0.1/24";
+ };
+
+ networking = {
+ useNetworkd = true;
+ useDHCP = false;
+ };
+
+ services.beszel.hub = {
+ enable = true;
+ host = "10.0.0.1";
+ };
+
+ networking.firewall.allowedTCPPorts = [
+ config.services.beszel.hub.port
+ ];
+
+ environment.systemPackages = [
+ config.services.beszel.hub.package
+ ];
+ };
+
+ agentHost =
+ { config, pkgs, ... }:
+ {
+ virtualisation.vlans = [ 1 ];
+
+ systemd.network.networks."01-eth1" = {
+ name = "eth1";
+ networkConfig.Address = "10.0.0.2/24";
+ };
+
+ networking = {
+ useNetworkd = true;
+ useDHCP = false;
+ };
+
+ environment.systemPackages = with pkgs; [ jq ];
+
+ specialisation."agent".configuration = {
+ services.beszel.agent = {
+ enable = true;
+ environment.HUB_URL = "http://10.0.0.1:8090";
+ environment.KEY_FILE = "/var/lib/beszel-agent/id_ed25519.pub";
+ environment.TOKEN_FILE = "/var/lib/beszel-agent/token";
+ openFirewall = true;
+ };
+ };
+ };
+ };
+
+ testScript =
+ { nodes, ... }:
+ let
+ hubCfg = nodes.hubHost.services.beszel.hub;
+ agentCfg = nodes.agentHost.specialisation."agent".configuration.services.beszel.agent;
+ in
+ ''
+ import json
+
+ start_all()
+
+ with subtest("Start hub"):
+ hubHost.wait_for_unit("beszel-hub.service")
+ hubHost.wait_for_open_port(${toString hubCfg.port}, "${toString hubCfg.host}")
+
+ with subtest("Register user"):
+ agentHost.succeed('curl -f --json \'${
+ builtins.toJSON {
+ email = "admin@example.com";
+ password = "password";
+ }
+ }\' "${agentCfg.environment.HUB_URL}/api/beszel/create-user"')
+ user = json.loads(agentHost.succeed('curl -f --json \'${
+ builtins.toJSON {
+ identity = "admin@example.com";
+ password = "password";
+ }
+ }\' ${agentCfg.environment.HUB_URL}/api/collections/users/auth-with-password').strip())
+
+ with subtest("Install agent credentials"):
+ agentHost.succeed("mkdir -p \"$(dirname '${agentCfg.environment.KEY_FILE}')\" \"$(dirname '${agentCfg.environment.TOKEN_FILE}')\"")
+ sshkey = agentHost.succeed(f"curl -H 'Authorization: {user["token"]}' -f ${agentCfg.environment.HUB_URL}/api/beszel/getkey | jq -r .key").strip()
+ utoken = agentHost.succeed(f"curl -H 'Authorization: {user["token"]}' -f ${agentCfg.environment.HUB_URL}/api/beszel/universal-token | jq -r .token").strip()
+ agentHost.succeed(f"echo '{sshkey}' > '${agentCfg.environment.KEY_FILE}'")
+ agentHost.succeed(f"echo '{utoken}' > '${agentCfg.environment.TOKEN_FILE}'")
+
+ with subtest("Register agent in hub"):
+ agentHost.succeed(f'curl -H \'Authorization: {user["token"]}\' -f --json \'{${
+ builtins.toJSON {
+ "host" = "10.0.0.2";
+ "name" = "agent";
+ "pkey" = "{sshkey}";
+ "port" = "45876";
+ "tkn" = "{utoken}";
+ "users" = ''{user['record']['id']}'';
+ }
+ }}\' "${agentCfg.environment.HUB_URL}/api/collections/systems/records"')
+
+ with subtest("Start agent"):
+ agentHost.succeed("/run/current-system/specialisation/agent/bin/switch-to-configuration switch")
+ agentHost.wait_for_unit("beszel-agent.service")
+ agentHost.wait_until_succeeds("journalctl -eu beszel-agent --grep 'SSH connection established'")
+ agentHost.wait_until_succeeds(f'curl -H \'Authorization: {user["token"]}\' -f ${agentCfg.environment.HUB_URL}/api/collections/systems/records | grep agentHost')
+ '';
+}
diff --git a/pkgs/by-name/be/beszel/package.nix b/pkgs/by-name/be/beszel/package.nix
index 628592190f4d..39e50c6da52f 100644
--- a/pkgs/by-name/be/beszel/package.nix
+++ b/pkgs/by-name/be/beszel/package.nix
@@ -4,6 +4,7 @@
fetchFromGitHub,
nix-update-script,
buildNpmPackage,
+ nixosTests,
}:
buildGoModule rec {
pname = "beszel";
@@ -64,7 +65,10 @@ buildGoModule rec {
mv $out/bin/hub $out/bin/beszel-hub
'';
- passthru.updateScript = nix-update-script { };
+ passthru = {
+ updateScript = nix-update-script { };
+ tests.nixos = nixosTests.beszel;
+ };
meta = {
homepage = "https://github.com/henrygd/beszel";
--
2.50.1 |
a7ca20d to
6402686
Compare
Co-authored-by: Mirza Arnaut <mirza.arnaut45@gmail.com>
Co-authored-by: Mirza Arnaut <mirza.arnaut45@gmail.com>
6402686 to
c7e3e45
Compare
|
I added @arunoruto as co-author to the commits since he proposed a lot of changes. I've also rebased on master and fixed a merge conflict. @h7x4 please check if beszel package has correct |
Beszel is a lightweight server monitoring platform that includes Docker statistics, historical data, and alert functions. It normally is deployed using docker, but due to golang, it's quite easy to deploy as systemd service as well.
While I've been testing this module in my homelab, I've noticed #379932 popped up, but that creates only agent (and is marked as WIP), while this has both agent + hub.
Things done
nix.conf? (See Nix manual)sandbox = relaxedsandbox = truenix-shell -p nixpkgs-review --run "nixpkgs-review rev HEAD". Note: all changes have to be committed, also see nixpkgs-review usage./result/bin/)Add a 👍 reaction to pull requests you find important.