Add interactive NetBox host installer
CI / php-store (push) Waiting to run
CI / python-components (push) Waiting to run

This commit is contained in:
2026-08-24 23:00:28 +02:00
parent a4cecec7b9
commit 541f107e1e
11 changed files with 551 additions and 8 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ The agent owns only these two configured files:
In the operator-owned NetBox `configuration.py`, add once, after the normal `PLUGINS` declaration:
```python
from store_plugins import STORE_PLUGINS
from netbox.store_plugins import STORE_PLUGINS
PLUGINS += STORE_PLUGINS
```
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "mrblake-netbox-store-agent"
version = "0.1.1"
version = "0.1.2"
description = "Fail-closed host agent for curated NetBox plugin lifecycle operations"
readme = "README.md"
requires-python = ">=3.11"
@@ -260,7 +260,11 @@ class OperationProcessor:
temp_root = self.config.paths.temp_dir
if temp_root.is_symlink():
raise PolicyError("temporary directory may not be a symlink")
temp_root.mkdir(parents=True, exist_ok=True, mode=0o700)
temp_root.mkdir(parents=True, exist_ok=True, mode=0o711)
# The dropped-privilege source builder needs traversal to its own
# 0700 operation directory. It cannot list this root, the journal,
# or the separately protected backup directory.
os.chmod(temp_root, 0o711)
with tempfile.TemporaryDirectory(prefix="operation-", dir=temp_root) as temporary:
self._event(operation_id, "artifact", "Downloading and verifying approved artifact")
artifact = self.store.download_release(plan, Path(temporary))
@@ -10,9 +10,9 @@ User=root
Group=root
ExecStart=/usr/local/bin/netbox-store-agent --config /etc/netbox-store-agent/agent.toml daemon
StateDirectory=netbox-store-agent
StateDirectoryMode=0700
StateDirectoryMode=0711
RuntimeDirectory=netbox-store-agent
RuntimeDirectoryMode=0750
RuntimeDirectoryMode=0755
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
+1 -1
View File
@@ -7,7 +7,7 @@ SocketMode=0660
SocketUser=root
# Replace with the group of the NetBox service.
SocketGroup=netbox
DirectoryMode=0750
DirectoryMode=0755
RemoveOnStop=true
[Install]
+4
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import os
import stat
import tempfile
import unittest
from pathlib import Path
@@ -56,6 +58,8 @@ class ExecutorTests(unittest.TestCase):
self.assertEqual(runner.commands, [])
self.assertIsNone(journal.get_managed_plugin("demo-plugin"))
self.assertFalse(config.paths.include_path.exists())
if os.name == "posix":
self.assertEqual(stat.S_IMODE(config.paths.temp_dir.stat().st_mode), 0o711)
def test_install_is_disabled_and_uses_fixed_pip_argv(self) -> None:
config = make_config(self.root, dry_run=False)
+22
View File
@@ -0,0 +1,22 @@
import unittest
from pathlib import Path
class SystemdUnitTests(unittest.TestCase):
def test_socket_directory_is_traversable_but_socket_remains_restricted(self):
socket_unit = Path("systemd/netbox-store-agent.socket").read_text(encoding="utf-8")
self.assertIn("DirectoryMode=0755", socket_unit)
self.assertIn("SocketMode=0660", socket_unit)
self.assertIn("SocketGroup=netbox", socket_unit)
def test_state_traversal_does_not_relax_service_umask(self):
service_unit = Path("systemd/netbox-store-agent.service").read_text(encoding="utf-8")
self.assertIn("StateDirectoryMode=0711", service_unit)
self.assertIn("RuntimeDirectoryMode=0755", service_unit)
self.assertIn("UMask=0077", service_unit)
if __name__ == "__main__":
unittest.main()