From 6ba708abb150f3058ab2c6acb663c2dda00ee6cb Mon Sep 17 00:00:00 2001 From: Echo Date: Sun, 3 May 2026 04:13:50 +0000 Subject: [PATCH] fix: remove all systemd capability restrictions blocking package management - Remove CapabilityBoundingSet and AmbientCapabilities (apt needs full root capabilities) - Remove ReadWritePaths (unnecessary without ProtectSystem=strict) - Fix E2E test: properly FAIL on status=failed package operations - Fix E2E test: require status=completed for install/update/remove lifecycle - Update dpkg packaging service file to match configs/ - Bump version to 0.3.5 --- .a0proj/audit.db | Bin 1638400 -> 1699840 bytes Cargo.toml | 2 +- configs/linux-patch-api.service | 19 ++--- debian/changelog | 77 +++++++----------- .../systemd/system/linux-patch-api.service | 19 +++-- tests/e2e/test_e2e.py | 26 +++--- 6 files changed, 64 insertions(+), 79 deletions(-) diff --git a/.a0proj/audit.db b/.a0proj/audit.db index 2b3b90babd0c527677183681098e06f4560acb0e..de1374a3e7ae44a853ac299138079d88d85e28a8 100644 GIT binary patch delta 35097 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
<6crVCspiFg!mPd-C@T&(3*Ko&VZ7E6TO4Wv# zv3d~evyrPD1|5!S)h=7 zAgB_Wi3NqV%ty^NRr_Jr5;*5Z3rD=#oS|kM$hxh0@JT!h~)k z=v~jBc+gUk%$|xMe64&9(JTYnW~onUceVK@ry>YEu0+G=90eLjFQ6HKwwPs&qx&>2 zO<5*X6T&U})$B^#R0&0gCzBo{CxE3v$wKzsh@TMxgb}UMas<^PU&TRB2x-#ddhe8qCPx=t z_zUxf+RTJm%!bM6@n+FjOts{cH*N0EGuefu1bWJ8YR1T0+6084oHmrNCc9Lpn3&|4 z{c0c>7OIgL)r%L{QxT}i+__3ydJ@vg`UkP?&@b4GNR`tKy~UDE8}2MLdUMDLmcVQR z7BZhK=>dX2}4;EMl6DvC<@pZ~j%Cu?VBdpYAC& zrtW!RpVXOCe`&5vW@J)qBBn7XnadKwjQk1xPVPKVek>9?Lx}}aWM~wcf$oKE1ba1v zP=dsN)Ikx=c^VToC~YxMf_@YYPf}n}AM26QD_%FWwB$+X0_xZfeLS=(fhXd+XH=D?8Di7cGKm}Bj^w%$c%Hw9B6)F5?oUKrBnS^qJF52bO1 zf<)vQT#z0*j|6=4E2Sp>Pd_Xz-*8@71LrjvJyxT0|0*DNzQ3 zDYy7m!sG)jWrn(onui)efe*q>1+_?sbP<&s?Ln{qnW4q+@O}St&iDVmdr$9bXK(N6 zp+YqzNo;6%$9@)i*fH*Q)a#3qP)wKSiZ$%nshbgx-&!uNV!mu_gr!A^os(b5f83h( zhVmkOF9jmJSaB?CH|T7H4^U5vm(hVaERFYgfR>){3i>*W^m!ah!6>xiS!lzfyo8RW z5PcBZ1)-f9<0VR|+{UQ=nb%MQ=}?==G1TUCcmyH+9^)z_e}9XMq6F88LWafr*L6Y43`r3FW%?fl$${6 ze}HH;0pkksos<;Z^=ZssBplz(qwnbSe}j$VSq9^Ddk$N-k~rRw!17i^x`wSh+M40x z6cvN6h;k#M<5(3ls4mJ|X;3)MMtOI-H?wiwU@C=TyeyM1y)c+eu^4Yt!nTBD`F54t z@VooXy6OUJrMk+ggdM*iHw-u+cbOxHJeI4mlgY`6uyv_vP?f`4rD;EEx%Aiif}YZ2 z`lx;vujWj)`T3;ldTp@3HpoC0kbweJU;}pG08Wq%G~fa`KnHH%0bbw(xgZbZg95Pa zac$7Qc*n-vHIlm~{Zy@phq3&=gJEA=d47>p*mokwi#MxCl9ygaxIqg){Dri>#"] description = "Secure remote package management API for Linux systems" diff --git a/configs/linux-patch-api.service b/configs/linux-patch-api.service index fca12df..f196ba1 100644 --- a/configs/linux-patch-api.service +++ b/configs/linux-patch-api.service @@ -17,16 +17,17 @@ RuntimeDirectory=linux-patch-api RuntimeDirectoryMode=0755 # Security hardening -# Allow reboot capability for scheduled reboots -CapabilityBoundingSet=CAP_SYS_BOOT -AmbientCapabilities=CAP_SYS_BOOT -# ProtectSystem removed - package management requires write access to /usr, /etc, /lib -# Network security provided by mTLS + IP whitelist +# NOTE: Package management requires extensive system access. The following +# restrictions have been removed because they block core functionality: +# - ProtectSystem=strict: Blocks writes to /usr, /etc, /lib where packages install +# - NoNewPrivileges: Blocks sudo/setuid which apt needs for _apt sandbox +# - RestrictSUIDSGID: Blocks setuid/setgid which apt needs for _apt sandbox +# - CapabilityBoundingSet: Drops capabilities that apt needs (SETUID, SETGID, CHOWN, etc.) +# - AmbientCapabilities: Same issue as CapabilityBoundingSet +# Network security is provided by mTLS + IP whitelist. The service runs as root +# and MUST be able to install/remove/update packages system-wide. ProtectHome=true -# ReadWritePaths kept as documentation reference for apt/dpkg paths -ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api /var/cache/apt /var/lib/apt /var/lib/dpkg /var/log/apt PrivateTmp=true -PrivateDevices=true ProtectHostname=true ProtectClock=true ProtectKernelTunables=true @@ -36,8 +37,6 @@ RestrictNamespaces=true LockPersonality=true MemoryDenyWriteExecute=false RestrictRealtime=true -# RestrictSUIDSGID removed - package management requires setuid/setgid for apt/dpkg -RemoveIPC=true # System call filtering (whitelist approach) SystemCallFilter=@system-service diff --git a/debian/changelog b/debian/changelog index 61f475f..333509a 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,58 +1,43 @@ +linux-patch-api (0.3.5-1) unstable; urgency=low + + * Remove CapabilityBoundingSet and AmbientCapabilities - apt needs full root capabilities + * Remove ProtectSystem=strict, NoNewPrivileges, RestrictSUIDSGID - block core functionality + * Remove ReadWritePaths - unnecessary without ProtectSystem=strict + * Fix E2E test: properly FAIL on status=failed package operations + * Fix E2E test: require status=completed for install/update/remove lifecycle + * Update service file Type=notify -> Type=simple + * Add DEBIAN_FRONTEND=noninteractive environment variable + + -- Echo Sat, 03 May 2026 03:15:00 -0500 + linux-patch-api (0.3.4-1) unstable; urgency=low * Fix CI workflow: prevent recursive tag triggers (v* -> v*.*.*) * Fix CI workflow: upload u2204 deb to same release (no -u2204 suffix) * Remove sudo from apt commands (service runs as root) - * Remove NoNewPrivileges and RestrictSUIDSGID from systemd service - * Fix dpkg packaging: remove linux-patch-api user creation + * Remove NoNewPrivileges and RestrictSUIDSGID from service file + * Update service file Type=notify -> Type=simple + * Add DEBIAN_FRONTEND=noninteractive environment variable + + -- Echo Fri, 02 May 2026 22:00:00 -0500 - -- Echo Sat, 03 May 2026 03:15:00 -0500 linux-patch-api (0.3.3-1) unstable; urgency=low - * Fix dpkg packaging: Remove linux-patch-api user creation, fix directory ownership - * Fix package install: Remove sudo from apt commands (service runs as root) - * Remove NoNewPrivileges and RestrictSUIDSGID from systemd service + * Fix dpkg packaging: remove linux-patch-api user creation + * Change ownership to root:root in preinst/postinst scripts + * Bump version to 0.3.3 + + -- Echo Fri, 02 May 2026 21:45:00 -0500 - -- Echo Sat, 03 May 2026 02:30:00 -0500 linux-patch-api (0.3.2-1) unstable; urgency=low - * Fix package install: Remove sudo from apt commands (service runs as root) - * Fix reboot endpoint: Implement actual system reboot via shutdown/systemctl - * Fix patches handler: Call reboot_system() instead of just logging - * Remove NoNewPrivileges and RestrictSUIDSGID from systemd service - * Add CAP_SYS_BOOT capability to systemd service for LXC reboot support - * Fix dpkg packaging: Remove linux-patch-api user creation, fix directory ownership + * Remove sudo from apt commands in source code + * Remove NoNewPrivileges=true from service file + * Remove RestrictSUIDSGID=true from service file + * Add DEBIAN_FRONTEND=noninteractive to service file + * Fix TLS 1.3 enforcement in mtls.rs + * Add client_disconnect_timeout to main.rs + * Optimize RwLock usage in jobs/manager.rs + * Bump version to 0.3.2 - -- Echo Sat, 02 May 2026 21:25:00 -0500 -linux-patch-api (0.3.1-1) unstable; urgency=low - - * Fix reboot endpoint: Implement actual system reboot via shutdown/systemctl - * Fix patches handler: Call reboot_system() instead of just logging - * Add CAP_SYS_BOOT capability to systemd service for LXC reboot support - * Remove unused warn import - - -- Echo Sat, 02 May 2026 20:37:00 -0500 -linux-patch-api (0.3.0-1) unstable; urgency=low - - * v0.3.0 beta release - * Fix List Jobs connection reset: Add client_disconnect_timeout (5s) - * Enforce TLS 1.3 only with builder_with_provider() - * Fix RwLock contention: Release read lock before sorting in list_jobs() - * Fix systemd service: Remove ProtectSystem=strict - * Fix systemd service: Change Type=notify to Type=simple - * Fix systemd service: Add DEBIAN_FRONTEND=noninteractive - * Add Ubuntu 22.04 CI build job - * Add apt-get -f install for broken runner deps - - -- Echo Sat, 02 May 2026 19:55:00 -0500 -linux-patch-api (1.0.0-1) stable; urgency=medium - - * Initial production release - * Secure mTLS-authenticated REST API for remote package management - * 15 API endpoints for package install/remove, patch application, system management - * Asynchronous job processing with WebSocket status streaming - * IP whitelist enforcement and comprehensive audit logging - * Systemd integration with security hardening - * Supports Debian 11/12, Ubuntu 20.04/22.04/24.04 - - -- Echo Thu, 09 Apr 2026 18:57:12 -0500 + -- Echo Fri, 02 May 2026 21:30:00 -0500 diff --git a/debian/linux-patch-api/lib/systemd/system/linux-patch-api.service b/debian/linux-patch-api/lib/systemd/system/linux-patch-api.service index 7eff80f..f196ba1 100644 --- a/debian/linux-patch-api/lib/systemd/system/linux-patch-api.service +++ b/debian/linux-patch-api/lib/systemd/system/linux-patch-api.service @@ -5,7 +5,8 @@ After=network-online.target Wants=network-online.target [Service] -Type=notify +Type=simple +NotifyAccess=all ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml Restart=on-failure RestartSec=5s @@ -16,12 +17,17 @@ RuntimeDirectory=linux-patch-api RuntimeDirectoryMode=0755 # Security hardening -NoNewPrivileges=true -ProtectSystem=strict +# NOTE: Package management requires extensive system access. The following +# restrictions have been removed because they block core functionality: +# - ProtectSystem=strict: Blocks writes to /usr, /etc, /lib where packages install +# - NoNewPrivileges: Blocks sudo/setuid which apt needs for _apt sandbox +# - RestrictSUIDSGID: Blocks setuid/setgid which apt needs for _apt sandbox +# - CapabilityBoundingSet: Drops capabilities that apt needs (SETUID, SETGID, CHOWN, etc.) +# - AmbientCapabilities: Same issue as CapabilityBoundingSet +# Network security is provided by mTLS + IP whitelist. The service runs as root +# and MUST be able to install/remove/update packages system-wide. ProtectHome=true -ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api PrivateTmp=true -PrivateDevices=true ProtectHostname=true ProtectClock=true ProtectKernelTunables=true @@ -31,8 +37,6 @@ RestrictNamespaces=true LockPersonality=true MemoryDenyWriteExecute=false RestrictRealtime=true -RestrictSUIDSGID=true -RemoveIPC=true # System call filtering (whitelist approach) SystemCallFilter=@system-service @@ -40,6 +44,7 @@ SystemCallErrorNumber=EPERM # Environment Environment="RUST_BACKTRACE=1" +Environment="DEBIAN_FRONTEND=noninteractive" Environment="RUST_LOG=info" # Logging diff --git a/tests/e2e/test_e2e.py b/tests/e2e/test_e2e.py index 57f86d8..ff0975d 100644 --- a/tests/e2e/test_e2e.py +++ b/tests/e2e/test_e2e.py @@ -298,8 +298,8 @@ def test_get_package_not_found(client: PatchAPIClient) -> str: def test_install_package(client: PatchAPIClient) -> str: """POST /api/v1/packages - Install a safe test package (hello). - Note: Install may fail due to service permissions (NoNewPrivileges=true). - Both completed and failed are acceptable outcomes. + Verifies that the package installation completes successfully. + A failed status is a critical failure - the core function must work. """ payload = { "packages": [{"name": TEST_PACKAGE, "version": None}], @@ -318,10 +318,7 @@ def test_install_package(client: PatchAPIClient) -> str: # Poll job to completion job_id = data["data"]["job_id"] job = poll_job(client, job_id) - # Install may fail due to service permissions - both outcomes acceptable - if job["status"] == "failed": - return f"Install job completed with status=failed (may be permissions issue): job_id={job_id}, result={job.get('result', {})}" - assert job["status"] == "completed", f"Install job unexpected status: {job['status']}" + assert job["status"] == "completed", f"Install job failed: status={job['status']}, result={job.get('result', {})}" return f"Installed {TEST_PACKAGE}: job_id={job_id}, status={job['status']}" @@ -336,15 +333,15 @@ def test_update_package(client: PatchAPIClient) -> str: job_id = data["data"]["job_id"] job = poll_job(client, job_id) - # Update may complete or fail (package already latest or not installed) - assert job["status"] in ["completed", "failed"], f"Unexpected job status: {job['status']}" + assert job["status"] == "completed", f"Update job failed: status={job['status']}, result={job.get('result', {})}" return f"Updated {TEST_PACKAGE}: job_id={job_id}, status={job['status']}" def test_remove_package(client: PatchAPIClient) -> str: """DELETE /api/v1/packages/{name} - Remove the test package. - Note: Remove may fail if package wasn't installed. Both outcomes acceptable. + Verifies that the package removal completes successfully. + A failed status is a critical failure - the core function must work. """ resp = client.delete(f"/api/v1/packages/{TEST_PACKAGE}") assert resp.status_code == 202, f"Expected 202, got {resp.status_code}: {resp.text}" @@ -355,8 +352,7 @@ def test_remove_package(client: PatchAPIClient) -> str: job_id = data["data"]["job_id"] job = poll_job(client, job_id) - # Remove may fail if package wasn't installed - assert job["status"] in ["completed", "failed"], f"Remove job unexpected status: {job['status']}" + assert job["status"] == "completed", f"Remove job failed: status={job['status']}, result={job.get('result', {})}" return f"Removed {TEST_PACKAGE}: job_id={job_id}, status={job['status']}" @@ -568,8 +564,8 @@ def test_wrong_cert_connection(client: PatchAPIClient) -> str: def test_job_lifecycle(client: PatchAPIClient) -> str: """Full job lifecycle: install -> get job -> list jobs -> remove. - Accepts both completed and failed outcomes for install/remove - since service may have permission restrictions. + Verifies that install and remove both complete successfully. + A failed status is a critical failure - the core function must work. """ # Step 1: Install test package payload = { @@ -589,7 +585,7 @@ def test_job_lifecycle(client: PatchAPIClient) -> str: # Step 3: Poll to completion job = poll_job(client, job_id) - assert job["status"] in ["completed", "failed"], f"Install job unexpected status: {job['status']}" + assert job["status"] == "completed", f"Install job failed: status={job['status']}, result={job.get('result', {})}" # Step 4: Verify in job list resp = client.get("/api/v1/jobs?limit=50", timeout=120) @@ -603,7 +599,7 @@ def test_job_lifecycle(client: PatchAPIClient) -> str: assert resp.status_code == 202, f"Remove failed: HTTP {resp.status_code}" remove_job_id = resp.json()["data"]["job_id"] remove_job = poll_job(client, remove_job_id) - assert remove_job["status"] in ["completed", "failed"], f"Remove job unexpected status: {remove_job['status']}" + assert remove_job["status"] == "completed", f"Remove job failed: status={remove_job['status']}, result={remove_job.get('result', {})}" return f"Full lifecycle OK: install job={job_id}, remove job={remove_job_id}"