Hello. I’m wondering if anyone has managed to get NinjaOne working on Nix, or could perhaps offer any insight into what I may have done wrong with my packaging attempt?
Frustratingly, it mostly works. I’ve gone through a through iterations, with and without FHS Envs.
Please excuse the fact I cannot provide the .deb package.
Things about NinjaOne and where I’m at:
-
It expects to be installed to
/opt/NinjaRMMAgent. -
It collects and sends data to its servers
- this is currently functional
- it collects CPU stats, memory, running processes, partition information, networks
-
It generates a unique to the system Machine ID
From what I can glean, this should be immutable (unless you physically install a new network card, say). The behaviour of the software is that it logs out (on signin/register) that there is a machine ID conflict and it generates itself a new NodeId to use for this device connecting in. In NinjaOne, the result is a duplicate entry under the hostname in their control panel. It does this not just on reboot but also if you simply restart the systemd service for their agent.
This may be the key bit. From wrapping it in strace, I found it attempts to get a machine ID for the system from dbus. That’d be from /etc/machine-id and it was also looking in the dbus location /var/lib/dbus/machine-id for this. (I’ve currently got a systemd tmpfiles symlink to make it readable.) I think it uses that value as an input to generate a V5 UUID. But I think perhaps it uses both the machine ID and a mac address from one of the network interfaces. Maybe it’s not able to read that value without being given some extra permissions?
I’m hoping that may mean something to one of you in the context of Nix. Does it just need some extra permission for its service to read appropriate devices?
Another thing is that I know autopatchelf is working correctly, so I don’t think there’s anything wrong there. Stracing it did lead me to think perhaps some of the libs weren’t right but it seemed from the output during build that they’re fine. I had a look at nix-alien as well but I think it’s mostly right and mostly working. I really do think it just doesn’t have access to some additional thing it wants but that I couldn’t see with strace.
I’m especially curious to learn anything transferable to packaging other stuff of this ilk. When we should use FHS, when we should use systemd permissions and mounts. Maybe I should be doing one but I’m doing the other.
This is what I’ve got so far. Please excuse anything weird, it’s partly me and partly Copilot. Let’s blame Copilot for anything that doesn’t look right ![]()
Both files live in modules/ninjaone.
ninjaone.nix
{
config,
lib,
pkgs,
...
}: let
cfg = config.services.ninjarmm-agent;
in {
options.services.ninjarmm-agent = {
enable = lib.mkEnableOption "ninja-one Agent";
package = lib.mkOption {
type = lib.types.package;
default =
(pkgs.callPackage ./default.nix {
numactl = pkgs.numactl;
readline = pkgs.readline;
ncurses = pkgs.ncurses;
}).agent;
description = "The ninja-one agent package to use.";
};
wrapper = lib.mkOption {
type = lib.types.package;
default =
(pkgs.callPackage ./default.nix {
numactl = pkgs.numactl;
readline = pkgs.readline;
ncurses = pkgs.ncurses;
}).wrapper;
description = "The ninja-one agent wrapper script.";
};
baseUrl = lib.mkOption {
type = lib.types.str;
description = "The base URL for the NinjaOne agent to connect to.";
example = "https://app.ninjarmm.com";
};
port = lib.mkOption {
type = lib.types.port;
description = "The port for the NinjaOne agent to connect on.";
default = 443;
};
clientUID = lib.mkOption {
type = lib.types.str;
description = "The client UID for the NinjaOne agent.";
};
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [cfg.package cfg.wrapper];
services.dbus.packages = [cfg.package cfg.wrapper];
# Set up environment variables for the agent
environment.variables = {
NINJA_AGENT_HOME = "${cfg.package}/libexec/NinjaRMMAgent";
};
# Use systemd-tmpfiles to create the /opt directory structure and log file
systemd.tmpfiles.rules = [
# Create directories
"d /opt/NinjaRMMAgent 0755 root root -"
"d /opt/NinjaRMMAgent/programfiles 0755 root root -"
"d /opt/NinjaRMMAgent/programfiles/config 0755 root root -"
"d /opt/NinjaRMMAgent/programdata 0755 root root -"
"d /opt/NinjaRMMAgent/programdata/storage 0755 root root -"
"d /opt/NinjaRMMAgent/programdata/download 0755 root root -"
# It attempts to read its own systemd service file, create a dummy for it to read from
"d /opt/NinjaRMMAgent/fake-systemd 0755 root root -"
"d /opt/NinjaRMMAgent/fake-systemd/system 0755 root root -"
"d /opt/NinjaRMMAgent/fake-systemd/system/multi-user.target.wants 0755 root root -"
"d /opt/NinjaRMMAgent/bin 0755 root root -"
"d /opt/NinjaRMMAgent/rootconfig 0755 root root -"
# Create log file for strace output
"f /var/log/ninjarmm-agent-strace.log 0644 root root -"
# Copy all files so they're writable (C+ = copy and replace if newer)
"C+ /opt/NinjaRMMAgent/programfiles/ninjarmm-linagent 0755 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent"
"C+ /opt/NinjaRMMAgent/programfiles/ninjarmm-linagent.manifest 0644 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent.manifest"
"C+ /opt/NinjaRMMAgent/programfiles/ninjarmm-linagent-patcher 0755 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent-patcher"
"C+ /opt/NinjaRMMAgent/programfiles/ninjarmm-linagent-patcher.manifest 0644 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent-patcher.manifest"
"C+ /opt/NinjaRMMAgent/programfiles/njbar 0755 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/njbar"
"C+ /opt/NinjaRMMAgent/programfiles/njbar.app.manifest 0644 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/njbar.app.manifest"
"C+ /opt/NinjaRMMAgent/programfiles/curl-ca-bundle.crt 0644 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/curl-ca-bundle.crt"
"C+ /opt/NinjaRMMAgent/programfiles/config/agent.conf 0644 root root - ${cfg.package}/libexec/NinjaRMMAgent/programfiles/config/agent.conf"
];
systemd.services.ninjarmm-agent = {
description = "NinjaOne Agent";
wantedBy = ["multi-user.target"];
after = ["network.target"];
preStart = ''
# Create the server.conf with dynamic configuration
${pkgs.coreutils}/bin/cat > /opt/NinjaRMMAgent/programfiles/config/server.conf << EOF
[General]
ClientUID=${cfg.clientUID}
Host=${cfg.baseUrl}
Port=${toString cfg.port}
DivisionUID=
NodeUid=
EOF
# Copy CA certificate bundles to /opt
${pkgs.coreutils}/bin/cp ${cfg.package}/libexec/NinjaRMMAgent/programfiles/curl-ca-bundle.crt /opt/NinjaRMMAgent/programfiles/
# Ensure the Qt config directory exists and is writable
${pkgs.coreutils}/bin/mkdir -p '/root/.config/Unknown Organization'
${pkgs.coreutils}/bin/touch '/root/.config/Unknown Organization/ninjarmm-linagent.conf'
${pkgs.coreutils}/bin/chmod 666 '/root/.config/Unknown Organization/ninjarmm-linagent.conf'
# Create a fake ldconfig that does nothing to avoid permission errors
${pkgs.coreutils}/bin/mkdir -p /opt/NinjaRMMAgent/bin
${pkgs.coreutils}/bin/cat > /opt/NinjaRMMAgent/bin/ldconfig << 'EOF'
#!/bin/bash
# Fake ldconfig that does nothing - libraries are already configured in NixOS
exit 0
EOF
${pkgs.coreutils}/bin/chmod +x /opt/NinjaRMMAgent/bin/ldconfig
# Also create fake ldconfig at /sbin and /usr/sbin in the FHS env
${pkgs.coreutils}/bin/mkdir -p /opt/NinjaRMMAgent/sbin /opt/NinjaRMMAgent/usr/sbin
${pkgs.coreutils}/bin/ln -sf /opt/NinjaRMMAgent/bin/ldconfig /opt/NinjaRMMAgent/sbin/ldconfig
${pkgs.coreutils}/bin/ln -sf /opt/NinjaRMMAgent/bin/ldconfig /opt/NinjaRMMAgent/usr/sbin/ldconfig
# Create symlinks from Nix store to /opt
# ${pkgs.coreutils}/bin/ln -sfn ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent /opt/NinjaRMMAgent/programfiles/
# ${pkgs.coreutils}/bin/ln -sfn ${cfg.package}/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent.manifest /opt/NinjaRMMAgent/programfiles/
'';
serviceConfig = {
Type = "simple";
User = "root";
# ExecStart = ''
# ${pkgs.strace}/bin/strace -tt -f -e open,openat,openat2,write,close -s 200 -o /var/log/ninjarmm-agent-strace.log \
# ${cfg.wrapper}/bin/ninjaone-agent
# '';
ExecStart = ''
${pkgs.strace}/bin/strace -tt -f -s 500 -o /var/log/ninjarmm-agent-strace.log \
${cfg.wrapper}/bin/ninjaone-agent
'';
Environment = [
"DAEMON_RUN=1"
"LC_ALL=C"
"NINJA_AGENT_HOME=/opt/NinjaRMMAgent"
"PATH=/opt/NinjaRMMAgent/bin:/usr/bin:/bin"
];
ProtectSystem = false;
ProtectHome = false;
PrivateDevices = false;
PrivateNetwork = false;
CapabilityBoundingSet = ["CAP_NET_RAW" "CAP_NET_ADMIN"];
WorkingDirectory = "/opt/NinjaRMMAgent/programfiles";
Restart = "always";
RestartSec = 5;
TimeoutStartSec = 10;
TimeoutStopSec = 90;
StandardOutput = "journal";
StandardError = "journal";
# State directories for runtime data
StateDirectory = "ninjarmm-agent";
StateDirectoryMode = "0755";
RuntimeDirectory = "ninjarmm-agent";
RuntimeDirectoryMode = "0755";
# Allow writing to /opt/NinjaRMMAgent and /var/log
ReadWritePaths = ["/opt/NinjaRMMAgent" "/var/log"];
};
};
};
}
default.nix
{
stdenv,
lib,
makeWrapper,
autoPatchelfHook,
libGL,
glibc,
gcc-unwrapped,
dpkg,
libxkbcommon,
xorg,
libxcb,
libX11,
mesa,
writeScriptBin,
bash,
# Required runtime dependencies
coreutils-full,
which,
parted,
networkmanager,
procps,
file,
util-linux,
nettools,
policycoreutils,
dmidecode,
python3,
xwayland,
openssl,
numactl,
readline,
ncurses,
attr,
acl,
gmp,
# libhistory is provided by readline
# libdl is provided by glibc
}: let
ninjaOne = stdenv.mkDerivation {
pname = "ninjaone";
version = "9.0.4181";
# src = ./NinjaOne-Agent-SeparateTesting-Remote-Auto-x86-64.deb;
src = ./NinjaOne-Agent-LinuxTesting-Remote-Auto-x86-64.deb;
nativeBuildInputs = [
autoPatchelfHook
dpkg
makeWrapper
];
buildInputs = [
libGL
glibc
gcc-unwrapped
stdenv.cc.cc.lib # For libgcc_s.so.1
libxkbcommon
libxkbcommon.out
xorg.libX11
xorg.libXext
xorg.libXrender
xorg.libXtst
xorg.libXinerama
xorg.libXrandr
xorg.libXcursor
xorg.libXi
xorg.libXScrnSaver
xorg.libXcomposite
xorg.libXdamage
xorg.libXfixes
# Additional dependencies from patchelf analysis
libxcb
libX11.out
# For libEGL.so.1
mesa
# Additional X11/systray dependencies for Wayland compatibility
xorg.libXau
xorg.libXdmcp
xorg.libxcb
xorg.libXinerama
xorg.libXrandr
xorg.libXxf86vm
# Wayland-X11 bridge support
xwayland
# Provide libmount.so.1, libblkid.so.1, and libcrypto
util-linux
openssl
# Provide libnuma.so
numactl
# Provide libreadline.so, libhistory.so
readline
# Provide libncursesw.so
ncurses
# libdl is provided by glibc
acl
gmp
attr
];
unpackPhase = ''
runHook preUnpack
mkdir -p ./extract
dpkg-deb -x $src ./extract
cd ./extract
runHook postUnpack
'';
installPhase = ''
runHook preInstall
# Create directory structure
mkdir -p $out/{bin,libexec}
# Copy all extracted files to libexec
cp -r ./opt/NinjaRMMAgent $out/libexec/
# Make binaries executable
find $out/libexec -type f -name "*.sh" -exec chmod +x {} \;
find $out/libexec/NinjaRMMAgent/programfiles -type f -executable -exec chmod +x {} \;
# Create direct wrapper script in bin
makeWrapper $out/libexec/NinjaRMMAgent/programfiles/ninjarmm-linagent $out/bin/ninjaone \
--prefix LD_LIBRARY_PATH : $out/libexec/NinjaRMMAgent/programfiles
# These were commented out once it became apparent autopatchelf is working correctly
# cp ${readline.out}/lib/libreadline.so.8 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${readline.out}/lib/libhistory.so.8 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${ncurses.out}/lib/libncursesw.so.6 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${stdenv.cc.libc.out}/lib/libc.so.6 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${stdenv.cc.libc.out}/lib/libdl.so.2 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${stdenv.cc.libc.out}/lib/libpthread.so.0 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${stdenv.cc.libc.out}/lib/librt.so.1 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${stdenv.cc.libc.out}/lib/libm.so.6 $out/libexec/NinjaRMMAgent/programfiles/
# # cp ${stdenv.cc.cc}/lib/libgcc_s.so.1 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${openssl.out}/lib/libcrypto.so.3 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${gmp.out}/lib/libgmp.so.10 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${acl.out}/lib/libacl.so.1 $out/libexec/NinjaRMMAgent/programfiles/
# cp ${attr.out}/lib/libattr.so.1 $out/libexec/NinjaRMMAgent/programfiles/
# Ensure the programdata directory exists
mkdir -p $out/libexec/NinjaRMMAgent/programdata/storage
# Don't modify the original config files - they'll be placed in /opt
# Copy CA bundle certificates from the source directory
cp ./tmp/ninja-startup/ninjarmm-curl-ca-bundle.crt $out/libexec/NinjaRMMAgent/programfiles/curl-ca-bundle.crt
cp ./tmp/ninja-startup/ninjarmm-curl-ca-bundle.crt $out/libexec/NinjaRMMAgent/programfiles/ninjarmm-curl-ca-bundle.crt
runHook postInstall
'';
# Debug information to help with build issues
dontStrip = true;
postFixup = ''
echo "Checking for patched binaries..."
find $out -type f -executable -exec file {} \; | grep ELF || echo "No ELF binaries found"
echo "Re-running autoPatchelf on programfiles..."
autoPatchelf $out/libexec/NinjaRMMAgent/programfiles/* || true
echo "RPATHs after patching:"
find $out/libexec/NinjaRMMAgent/programfiles -type f -executable -exec patchelf --print-rpath {} \; || true
'';
meta = with lib; {
description = "NinjaOne Agent";
license = licenses.unfree;
platforms = platforms.linux;
};
};
# Create a wrapper script that sets up the environment and runs the agent
ninjaOneWrapper = writeScriptBin "ninjaone-agent" ''
#!${bash}/bin/bash
# Set up environment variables
export LD_LIBRARY_PATH=${ninjaOne}/libexec/NinjaRMMAgent/programfiles:$LD_LIBRARY_PATH
export LC_ALL=C
# Make sure the agent can find system utilities
export PATH=${lib.makeBinPath [
# Core utilities required by the agent
coreutils-full # ls, who, cat
bash # bash shell
parted # disk partition monitoring
procps # top, ps, pgrep - process monitoring
dmidecode # bios information
numactl
readline
ncurses
util-linux # lsblk - Disk info
nettools # netstat - gateway lookup
networkmanager # nmcli - network stats
policycoreutils # sestatus - selinux operation
file # file - file type identification
python3 # for patching
which
]}:$PATH:/run/current-system/sw/bin:/run/wrappers/bin
# Debug information
echo "Starting NinjaOne Agent..."
# Run the agent from /opt as expected by the package
cd /opt/NinjaRMMAgent/programfiles
echo "Working directory: $(pwd)"
exec ./ninjarmm-linagent "$@"
'';
in {
agent = ninjaOne;
wrapper = ninjaOneWrapper;
}