nixos/paretosecurity: Add support for declarative linking

Before this PR, users that wanted to link their Pareto Desktop
to Pareto Cloud had to imperatively run `paretosecurity link ...`.
This PR adds a oneshot systemd service that does the linking
so the entire Pareto app can be configured declaratively with nix.
This commit is contained in:
Neyts Zupan
2025-08-11 07:01:59 +00:00
parent da0daef575
commit 93403bdacd
3 changed files with 133 additions and 49 deletions

View File

@@ -17,6 +17,25 @@ in
default = true;
description = "Set to false to disable the tray icon and run as a CLI tool only.";
};
users = lib.mkOption {
type = lib.types.attrsOf (
lib.types.submodule {
options = {
inviteId = lib.mkOption {
type = lib.types.str;
description = ''
A unique ID that links the agent to Pareto Cloud.
Get it from the Join Team page on `https://cloud.paretosecurity.com/team/join/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx`.
In Step 2, under Linux tab, enter your email then copy it from the generated command.
'';
example = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
};
};
}
);
default = { };
description = "Per-user Pareto Security configuration.";
};
};
config = lib.mkIf cfg.enable {
@@ -38,9 +57,64 @@ in
# if one is installed.
# The `paretosecurity-user` timer service that is configured lower has
# the same need.
systemd.services.paretosecurity.serviceConfig.Environment = [
"PATH=${config.system.path}/bin:${config.system.path}/sbin"
];
systemd.services = {
paretosecurity.serviceConfig.Environment = [
"PATH=${config.system.path}/bin:${config.system.path}/sbin"
];
}
// (
# Each user can set their inviteID, which creates a systemd service
# that runs `paretosecurity link ...` to link their device to Pareto Cloud.
lib.mapAttrs' (
username: userConfig:
lib.nameValuePair "paretosecurity-link-${username}" {
description = "Link Pareto Desktop to Pareto Cloud for user ${username}";
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
serviceConfig = {
Type = "oneshot";
RemainAfterExit = true;
User = username;
StateDirectory = "paretosecurity/${username}";
ExecStart = pkgs.writeShellScript "paretosecurity-link-${username}" ''
set -euo pipefail
INVITE_ID="${userConfig.inviteId}"
STATE_FILE="/var/lib/paretosecurity/${username}/linked-$INVITE_ID"
CONFIG_FILE="$HOME/.config/pareto.toml"
# Check if already linked with this specific invite
if [ -f "$STATE_FILE" ]; then
echo "Device already linked with invite $INVITE_ID for user ${username}"
exit 0
fi
# Ensure config directory exists
mkdir -p "$(dirname "$CONFIG_FILE")"
# Perform linking
echo "Linking device to Pareto Cloud for user ${username}..."
${cfg.package}/bin/paretosecurity link \
"paretosecurity://linkDevice/?invite_id=$INVITE_ID"
# Verify linking succeeded
if [ -f "$CONFIG_FILE" ] && grep -q "TeamID" "$CONFIG_FILE"; then
echo "Successfully linked to Pareto Cloud for user ${username}"
touch "$STATE_FILE"
else
echo "Failed to link to Pareto Cloud for user ${username}"
exit 1
fi
'';
};
wantedBy = [ "multi-user.target" ];
}
) cfg.users
);
# Enable the tray icon and timer services if the trayIcon option is enabled
systemd.user = lib.mkIf cfg.trayIcon {

View File

@@ -9,7 +9,7 @@
imports = [ ./common/user-account.nix ];
services.paretosecurity.enable = true;
services.paretosecurity.users.alice.inviteId = "test-invite-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx";
};
nodes.xfce =
@@ -46,46 +46,52 @@
terminal.systemctl("start network-online.target")
terminal.wait_for_unit("network-online.target")
# Test 1: Test the systemd socket is installed & enabled
terminal.succeed('systemctl is-enabled paretosecurity.socket')
with subtest("Test the systemd socket is installed & enabled"):
terminal.succeed('systemctl is-enabled paretosecurity.socket')
# Test 2: Test running checks
terminal.succeed(
"su - alice -c 'paretosecurity check"
# Disable some checks that need intricate test setup so that this test
# remains simple and fast. Tests for all checks and edge cases available
# at https://github.com/ParetoSecurity/agent/tree/main/test/integration
+ " --skip c96524f2-850b-4bb9-abc7-517051b6c14e" # SecureBoot
+ " --skip 37dee029-605b-4aab-96b9-5438e5aa44d8" # Screen lock
+ " --skip 21830a4e-84f1-48fe-9c5b-beab436b2cdb" # Disk encryption
+ " --skip 44e4754a-0b42-4964-9cc2-b88b2023cb1e" # Pareto Security is up to date
+ " --skip f962c423-fdf5-428a-a57a-827abc9b253e" # Password manager installed
+ "'"
)
with subtest("Test running checks"):
terminal.succeed(
"su - alice -c 'paretosecurity check"
# Disable some checks that need intricate test setup so that this test
# remains simple and fast. Tests for all checks and edge cases available
# at https://github.com/ParetoSecurity/agent/tree/main/test/integration
+ " --skip c96524f2-850b-4bb9-abc7-517051b6c14e" # SecureBoot
+ " --skip 37dee029-605b-4aab-96b9-5438e5aa44d8" # Screen lock
+ " --skip 21830a4e-84f1-48fe-9c5b-beab436b2cdb" # Disk encryption
+ " --skip 44e4754a-0b42-4964-9cc2-b88b2023cb1e" # Pareto Security is up to date
+ " --skip f962c423-fdf5-428a-a57a-827abc9b253e" # Password manager installed
+ "'"
)
# Test 3: Test the tray icon
xfce.wait_for_x()
for unit in [
'paretosecurity-trayicon',
'paretosecurity-user',
'paretosecurity-user.timer'
]:
status, out = xfce.systemctl("is-enabled " + unit, "alice")
assert status == 0, f"Unit {unit} is not enabled (status: {status}): {out}"
xfce.succeed("xdotool mousemove 460 10")
xfce.wait_for_text("Pareto Security")
xfce.succeed("xdotool click 1")
xfce.wait_for_text("Run Checks")
with subtest("Test linking to Pareto Cloud"):
# The linking service will fail because there is no Internet,
# but we can check that it tried
terminal.succeed('systemctl list-units --type=service | grep paretosecurity-link-alice')
terminal.succeed('journalctl -u paretosecurity-link-alice.service | grep "Linking device to Pareto Cloud for user alice"')
# Test 4: Desktop entry
xfce.succeed("xdotool mousemove 10 10")
xfce.succeed("xdotool click 1") # hide the tray icon window
xfce.succeed("xdotool click 1") # show the Applications menu
xfce.succeed("xdotool mousemove 10 200")
xfce.succeed("xdotool click 1")
xfce.wait_for_text("Pareto Security")
with subtest("Test 3: Test the tray icon"):
xfce.wait_for_x()
for unit in [
'paretosecurity-trayicon',
'paretosecurity-user',
'paretosecurity-user.timer'
]:
status, out = xfce.systemctl("is-enabled " + unit, "alice")
assert status == 0, f"Unit {unit} is not enabled (status: {status}): {out}"
xfce.succeed("xdotool mousemove 460 10")
xfce.wait_for_text("Pareto Security")
xfce.succeed("xdotool click 1")
xfce.wait_for_text("Run Checks")
# Test 5: paretosecurity:// URL handler is registered
xfce.succeed("su - alice -c 'xdg-open paretosecurity://foo'")
with subtest("Test 4: Desktop entry"):
xfce.succeed("xdotool mousemove 10 10")
xfce.succeed("xdotool click 1") # hide the tray icon window
xfce.succeed("xdotool click 1") # show the Applications menu
xfce.succeed("xdotool mousemove 10 200")
xfce.succeed("xdotool click 1")
xfce.wait_for_text("Pareto Security")
with subtest("Test 5: paretosecurity:// URL handler is registered"):
xfce.succeed("su - alice -c 'xdg-open paretosecurity://foo'")
'';
}

View File

@@ -78,13 +78,15 @@ buildGoModule (finalAttrs: {
};
meta = {
description = "Agent that makes sure your laptop is correctly configured for security";
description = "A simple trayicon app that makes sure your laptop is correctly configured for security";
longDescription = ''
The Pareto Security agent is a free and open source app to help you make
sure that your laptop is configured for security.
[Pareto Desktop](https://paretosecurity.com/linux) is a free and open
source trayicon app to help you configure your laptop for security. It
nudges you to take care of 20% of security-related tasks that bring 80% of
protection.
By default, it's a CLI command that prints out a report on basic security
settings such as if you have disk encryption and firewall enabled.
In it's simplest form, it's a CLI command that prints out a report on basic
security settings such as if you have disk encryption and firewall enabled.
If you use the `services.paretosecurity` NixOS module, you also get a
root helper that allows you to run the checker in userspace. Some checks
@@ -96,9 +98,11 @@ buildGoModule (finalAttrs: {
once per hour. If you want to use just the CLI mode, set
`services.paretosecurity.trayIcon` to `false`.
Finally, you can run `paretosecurity link` to configure the agent
to send the status of checks to https://dash.paretosecurity.com to make
compliance people happy. No sending happens until your device is linked.
Finally, if you set `users.users.alice.paretosecurity.inviteId = "..."`
to the `inviteId` you get on [Pareto Cloud](https://cloud.paretosecurity.com/),
then Pareto Desktop acts as a read-only and push-only agent that sends the
status of checks to https://cloud.paretosecurity.com which makes
compliance people happy and your privacy intact.
'';
homepage = "https://github.com/ParetoSecurity/agent";
license = lib.licenses.gpl3Only;