update
This commit is contained in:
@@ -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.
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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']}"
|
||||
)
|
||||
|
||||
@@ -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 <admin@robot.package>
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user