diff --git a/agent/README.md b/agent/README.md index c5ff776..3f13a30 100644 --- a/agent/README.md +++ b/agent/README.md @@ -2,7 +2,7 @@ FastAPI service that runs on each Linux client and listens on `127.0.0.1:5010`. -It accepts install, update, remove, task, log, installed-app, and service-control requests from `robot.installer`. It stores state in local SQLite and installs trusted `.deb` components downloaded from `robot.package`, allowlisted Ubuntu APT packages, plus Docker image components from allowed registries when Docker support is enabled. +It accepts install, update, remove, task, log, installed-app, and service-control requests from `robot.installer`. It stores state in local SQLite and installs trusted `.deb` components downloaded from `robot.package`, Ubuntu APT packages, plus Docker image components from allowed registries when Docker support is enabled. ## Development @@ -88,7 +88,28 @@ APT components use a fixed manifest contract and never accept shell commands: } ``` -The package name must be present in `ALLOWED_APT_PACKAGES` (default: `postgresql`). After installation, the Agent discovers concrete systemd `.service` units shipped by the trusted package and reads their state with a fixed `systemctl show` argv (no shell command is accepted from the manifest or Web Client). The structured result is returned as `serviceCheckStatus` and `serviceChecks` from the task component API and is retained with the installed app for the UI. +`ALLOWED_APT_PACKAGES=*` allows every syntactically valid APT package name and is the default for newly built Agents. This lets an administrator enter an exact package code on the Web Server without updating each Agent's configuration for every new package. + +To enable this behavior on an already deployed Agent: + +```bash +sudo sed -i \ + 's/^ALLOWED_APT_PACKAGES=.*/ALLOWED_APT_PACKAGES=*/' \ + /etc/local-installer-agent/agent.env +sudo systemctl restart local-installer-agent +``` + +To restore a restricted allowlist, use a comma-separated list without spaces: + +```bash +ALLOWED_APT_PACKAGES=postgresql,nginx,redis-server,curl +``` + +Every allowed package must be available from the APT sources configured on the target client. The Agent runs `apt-get update`, but it does not add third-party repositories or signing keys. + +APT packages can execute maintainer scripts as root during installation. Wildcard mode should therefore only be used when the Web Server and package-upload accounts are trusted and access-controlled. + +After installation, the Agent discovers concrete systemd `.service` units shipped by the trusted package and reads their state with a fixed `systemctl show` argv (no shell command is accepted from the manifest or Web Client). The structured result is returned as `serviceCheckStatus` and `serviceChecks` from the task component API and is retained with the installed app for the UI. PostgreSQL keeps an additional trusted policy: the Agent enables and starts `postgresql.service`, verifies that it is active, and waits for `pg_isready`. Other discovered package services are checked read-only; they are not blindly enabled or started because packages can ship optional or one-shot units. diff --git a/agent/app/config.py b/agent/app/config.py index 2185a69..699645c 100644 --- a/agent/app/config.py +++ b/agent/app/config.py @@ -63,7 +63,7 @@ def _bool(name: str, default: bool) -> bool: def get_settings() -> Settings: robot_package_base_url = os.getenv("ROBOT_PACKAGE_BASE_URL", "https://package.pnkr.cloud").rstrip("/") return Settings( - agent_version=os.getenv("AGENT_VERSION", "1.0.3"), + agent_version=os.getenv("AGENT_VERSION", "1.0.4"), host=os.getenv("AGENT_HOST", "0.0.0.0"), port=int(os.getenv("AGENT_PORT", "5010")), robot_package_base_url=robot_package_base_url, @@ -86,7 +86,7 @@ def get_settings() -> Settings: ), allowed_apt_packages=_csv( os.getenv("ALLOWED_APT_PACKAGES"), - ["postgresql"], + ["*"], ), allowed_docker_registries=_csv_with_defaults( os.getenv("ALLOWED_DOCKER_REGISTRIES"), diff --git a/agent/app/core/installer.py b/agent/app/core/installer.py index 9a88342..ce93083 100644 --- a/agent/app/core/installer.py +++ b/agent/app/core/installer.py @@ -128,8 +128,8 @@ class AptInstaller(DebInstaller): def discover_service_units(self, package_name: str) -> list[str]: """Return concrete systemd service units shipped by an installed package. - The package name is already constrained by the Agent APT allowlist. It is - still passed as a single argv item and no shell expansion is performed. + The package name is already syntax-validated by the manifest validator. It + is passed as a single argv item and no shell expansion is performed. Template units are omitted because they cannot be meaningfully checked without an instance name. """ diff --git a/agent/app/core/manifest_validator.py b/agent/app/core/manifest_validator.py index 48ab642..e1c103a 100644 --- a/agent/app/core/manifest_validator.py +++ b/agent/app/core/manifest_validator.py @@ -18,7 +18,7 @@ class ManifestValidator: elif component_type == "apt": component = AptComponent.model_validate(raw_component).model_dump(by_alias=True) allowed_packages = set(settings.allowed_apt_packages) - if component["packageName"] not in allowed_packages: + if "*" not in allowed_packages and component["packageName"] not in allowed_packages: raise ValueError( f"APT package is not allowed: {component['packageName']}" ) diff --git a/agent/packaging/DEBIAN/control b/agent/packaging/DEBIAN/control index 9cba583..af53d95 100644 --- a/agent/packaging/DEBIAN/control +++ b/agent/packaging/DEBIAN/control @@ -1,5 +1,5 @@ Package: local-installer-agent -Version: 1.0.3 +Version: 1.0.4 Section: utils Priority: optional Architecture: amd64 @@ -7,4 +7,4 @@ Maintainer: Robot Team Depends: python3, python3-venv, python3-pip, curl Description: Local Installer Agent for robot.installer A local background service that installs, updates, and removes trusted .deb, - allowlisted APT, and Docker apps on the user's Linux machine. + APT repository packages, and Docker apps on the user's Linux machine. diff --git a/agent/scripts/build-deb.sh b/agent/scripts/build-deb.sh index 14c0b72..12482fc 100644 --- a/agent/scripts/build-deb.sh +++ b/agent/scripts/build-deb.sh @@ -1,10 +1,11 @@ #!/usr/bin/env bash set -euo pipefail -VERSION="${VERSION:-1.0.3}" +VERSION="${VERSION:-1.0.4}" ARCH="${ARCH:-amd64}" AGENT_HOST="${AGENT_HOST:-0.0.0.0}" AGENT_PORT="${AGENT_PORT:-5010}" +ALLOWED_APT_PACKAGES="${ALLOWED_APT_PACKAGES:-*}" DEB_COMPRESSION="${DEB_COMPRESSION:-gzip}" PKG_NAME="local-installer-agent" BUILD_ROOT="${BUILD_ROOT:-build}" @@ -21,6 +22,12 @@ if [[ ! "$ARCH" =~ ^[a-z0-9][a-z0-9._-]*$ ]]; then exit 1 fi +if [[ "$ALLOWED_APT_PACKAGES" != "*" && ! "$ALLOWED_APT_PACKAGES" =~ ^[a-zA-Z0-9._+-]+(,[a-zA-Z0-9._+-]+)*$ ]]; then + echo "Invalid ALLOWED_APT_PACKAGES: ${ALLOWED_APT_PACKAGES}" >&2 + echo "Use * for any valid APT package or a comma-separated list without spaces." >&2 + exit 1 +fi + rm -rf "${BUILD_DIR}" rm -f "${OUTPUT_PACKAGE}" @@ -61,7 +68,7 @@ AGENT_PORT=${AGENT_PORT} ROBOT_PACKAGE_BASE_URL=https://package.pnkr.cloud ALLOWED_ORIGINS=https://app.pnkr.cloud,https://package.pnkr.cloud,http://localhost:3000,http://127.0.0.1:3000,http://localhost:5173,http://127.0.0.1:5173,http://localhost:8080,http://127.0.0.1:8080 ALLOWED_DOWNLOAD_HOSTS=package.pnkr.cloud -ALLOWED_APT_PACKAGES=postgresql +ALLOWED_APT_PACKAGES=${ALLOWED_APT_PACKAGES} ALLOWED_DOCKER_REGISTRIES=registry.robot.package,docker.io CACHE_DIR=/var/cache/local-installer-agent/packages APP_DIR=/opt/robot-apps diff --git a/agent/tests/test_apt_components.py b/agent/tests/test_apt_components.py index fafa82d..a6abfee 100644 --- a/agent/tests/test_apt_components.py +++ b/agent/tests/test_apt_components.py @@ -46,6 +46,21 @@ class FakeCommandRunner: class AptManifestTests(unittest.TestCase): + def test_wildcard_accepts_any_valid_apt_package_name(self) -> None: + validator_settings = SimpleNamespace(allowed_apt_packages=["*"]) + with patch("app.core.manifest_validator.settings", validator_settings): + manifest = ManifestValidator().validate( + apt_manifest( + { + "componentId": "nginx-extras", + "type": "apt", + "packageName": "nginx-extras", + } + ) + ) + + self.assertEqual(manifest["components"][0]["packageName"], "nginx-extras") + def test_postgresql_is_accepted_when_allowlisted(self) -> None: validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"]) with patch("app.core.manifest_validator.settings", validator_settings): @@ -54,6 +69,23 @@ class AptManifestTests(unittest.TestCase): self.assertEqual(manifest["components"][0]["type"], "apt") self.assertEqual(manifest["components"][0]["packageName"], "postgresql") + def test_any_package_in_a_multi_package_allowlist_is_accepted(self) -> None: + validator_settings = SimpleNamespace( + allowed_apt_packages=["postgresql", "nginx", "redis-server"] + ) + with patch("app.core.manifest_validator.settings", validator_settings): + manifest = ManifestValidator().validate( + apt_manifest( + { + "componentId": "redis-server", + "type": "apt", + "packageName": "redis-server", + } + ) + ) + + self.assertEqual(manifest["components"][0]["packageName"], "redis-server") + def test_non_allowlisted_package_is_rejected(self) -> None: validator_settings = SimpleNamespace(allowed_apt_packages=["postgresql"]) with patch("app.core.manifest_validator.settings", validator_settings): diff --git a/web-server/views/partials/package-modal.ejs b/web-server/views/partials/package-modal.ejs index ab44be1..da2e49e 100644 --- a/web-server/views/partials/package-modal.ejs +++ b/web-server/views/partials/package-modal.ejs @@ -62,7 +62,7 @@
- APT packages only store metadata and do not need an uploaded file. Package code must be an Agent-allowlisted APT name, for example postgresql. + APT packages only store metadata and do not need an uploaded file. Enter the exact package name accepted by apt-get install, for example postgresql, nginx, or redis-server. Agents using ALLOWED_APT_PACKAGES=* accept any valid package code.