Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6ba708abb1 | |||
| de7ec9905f | |||
| 508037d656 | |||
| 56de1d73e1 | |||
| 157376af7e | |||
| 77e8ac2e65 | |||
| 9e42f32270 |
BIN
.a0proj/audit.db
BIN
.a0proj/audit.db
Binary file not shown.
@ -3,7 +3,7 @@ name: CI/CD Pipeline
|
|||||||
"on":
|
"on":
|
||||||
push:
|
push:
|
||||||
branches: [ master, develop ]
|
branches: [ master, develop ]
|
||||||
tags: [ 'v*' ]
|
tags: [ 'v*.*.*' ]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
|
|
||||||
@ -162,8 +162,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||||
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
FILE=$(ls ../linux-patch-api_*.deb 2>/dev/null | head -1)
|
||||||
|
# Rename deb to include u2204 in filename to avoid collision with main build
|
||||||
|
if [ -n "$FILE" ]; then
|
||||||
|
U2204_FILE="$(echo "$FILE" | sed 's/_amd64/_u2204_amd64/')"
|
||||||
|
mv "$FILE" "$U2204_FILE"
|
||||||
|
FILE="$U2204_FILE"
|
||||||
|
fi
|
||||||
chmod +x scripts/upload-release.sh
|
chmod +x scripts/upload-release.sh
|
||||||
./scripts/upload-release.sh "${TAG_NAME}-u2204" "$FILE"
|
./scripts/upload-release.sh "$TAG_NAME" "$FILE"
|
||||||
|
|
||||||
build-rpm:
|
build-rpm:
|
||||||
name: Build RPM Package
|
name: Build RPM Package
|
||||||
|
|||||||
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -1859,7 +1859,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.1"
|
version = "0.3.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix",
|
"actix",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "linux-patch-api"
|
name = "linux-patch-api"
|
||||||
version = "0.3.1"
|
version = "0.3.5"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Echo <echo@moon-dragon.us>"]
|
authors = ["Echo <echo@moon-dragon.us>"]
|
||||||
description = "Secure remote package management API for Linux systems"
|
description = "Secure remote package management API for Linux systems"
|
||||||
|
|||||||
@ -17,17 +17,17 @@ RuntimeDirectory=linux-patch-api
|
|||||||
RuntimeDirectoryMode=0755
|
RuntimeDirectoryMode=0755
|
||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
NoNewPrivileges=true
|
# NOTE: Package management requires extensive system access. The following
|
||||||
# Allow reboot capability for scheduled reboots
|
# restrictions have been removed because they block core functionality:
|
||||||
CapabilityBoundingSet=CAP_SYS_BOOT
|
# - ProtectSystem=strict: Blocks writes to /usr, /etc, /lib where packages install
|
||||||
AmbientCapabilities=CAP_SYS_BOOT
|
# - NoNewPrivileges: Blocks sudo/setuid which apt needs for _apt sandbox
|
||||||
# ProtectSystem removed - package management requires write access to /usr, /etc, /lib
|
# - RestrictSUIDSGID: Blocks setuid/setgid which apt needs for _apt sandbox
|
||||||
# Network security provided by mTLS + IP whitelist
|
# - 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
|
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
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
|
||||||
ProtectHostname=true
|
ProtectHostname=true
|
||||||
ProtectClock=true
|
ProtectClock=true
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
@ -37,8 +37,6 @@ RestrictNamespaces=true
|
|||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
MemoryDenyWriteExecute=false
|
MemoryDenyWriteExecute=false
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
RestrictSUIDSGID=true
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
# System call filtering (whitelist approach)
|
# System call filtering (whitelist approach)
|
||||||
SystemCallFilter=@system-service
|
SystemCallFilter=@system-service
|
||||||
|
|||||||
63
debian/changelog
vendored
63
debian/changelog
vendored
@ -1,32 +1,43 @@
|
|||||||
linux-patch-api (0.3.1-1) unstable; urgency=low
|
linux-patch-api (0.3.5-1) unstable; urgency=low
|
||||||
|
|
||||||
* Fix reboot endpoint: Implement actual system reboot via shutdown/systemctl
|
* Remove CapabilityBoundingSet and AmbientCapabilities - apt needs full root capabilities
|
||||||
* Fix patches handler: Call reboot_system() instead of just logging
|
* Remove ProtectSystem=strict, NoNewPrivileges, RestrictSUIDSGID - block core functionality
|
||||||
* Add CAP_SYS_BOOT capability to systemd service for LXC reboot support
|
* Remove ReadWritePaths - unnecessary without ProtectSystem=strict
|
||||||
* Remove unused warn import
|
* 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 <echo@moon-dragon.us> Sat, 02 May 2026 20:37:00 -0500
|
-- Echo <echo@moon-dragon.us> Sat, 03 May 2026 03:15:00 -0500
|
||||||
linux-patch-api (0.3.0-1) unstable; urgency=low
|
|
||||||
|
|
||||||
* v0.3.0 beta release
|
linux-patch-api (0.3.4-1) unstable; urgency=low
|
||||||
* 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 <echo@moon-dragon.us> Sat, 02 May 2026 19:55:00 -0500
|
* Fix CI workflow: prevent recursive tag triggers (v* -> v*.*.*)
|
||||||
linux-patch-api (1.0.0-1) stable; urgency=medium
|
* 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 service file
|
||||||
|
* Update service file Type=notify -> Type=simple
|
||||||
|
* Add DEBIAN_FRONTEND=noninteractive environment variable
|
||||||
|
|
||||||
* Initial production release
|
-- Echo <echo@moon-dragon.us> Fri, 02 May 2026 22:00:00 -0500
|
||||||
* 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 <echo@moon-dragon.us> Thu, 09 Apr 2026 18:57:12 -0500
|
linux-patch-api (0.3.3-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* 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 <echo@moon-dragon.us> Fri, 02 May 2026 21:45:00 -0500
|
||||||
|
|
||||||
|
linux-patch-api (0.3.2-1) unstable; urgency=low
|
||||||
|
|
||||||
|
* 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 <echo@moon-dragon.us> Fri, 02 May 2026 21:30:00 -0500
|
||||||
|
|||||||
4
debian/linux-patch-api/DEBIAN/postinst
vendored
4
debian/linux-patch-api/DEBIAN/postinst
vendored
@ -13,14 +13,14 @@ if [ "$1" = "configure" ]; then
|
|||||||
echo "Creating default config.yaml..."
|
echo "Creating default config.yaml..."
|
||||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
chmod 640 /etc/linux_patch_api/config.yaml
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/config.yaml
|
chown root:root /etc/linux_patch_api/config.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
echo "Creating default whitelist.yaml..."
|
echo "Creating default whitelist.yaml..."
|
||||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/whitelist.yaml
|
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reload systemd daemon to pick up new service file
|
# Reload systemd daemon to pick up new service file
|
||||||
|
|||||||
12
debian/linux-patch-api/DEBIAN/postrm
vendored
12
debian/linux-patch-api/DEBIAN/postrm
vendored
@ -39,18 +39,6 @@ if [ "$1" = "purge" ]; then
|
|||||||
rm -rf /var/log/linux_patch_api
|
rm -rf /var/log/linux_patch_api
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove system user
|
|
||||||
if getent passwd linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Removing user linux-patch-api..."
|
|
||||||
userdel linux-patch-api 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove system group
|
|
||||||
if getent group linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Removing group linux-patch-api..."
|
|
||||||
groupdel linux-patch-api 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "linux-patch-api purged successfully"
|
echo "linux-patch-api purged successfully"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
23
debian/linux-patch-api/DEBIAN/preinst
vendored
23
debian/linux-patch-api/DEBIAN/preinst
vendored
@ -9,31 +9,14 @@ if [ -d "/etc/linux_patch_api" ]; then
|
|||||||
echo "Detected existing installation - performing upgrade"
|
echo "Detected existing installation - performing upgrade"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create system user if it doesn't exist
|
|
||||||
if ! getent group linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Creating group linux-patch-api..."
|
|
||||||
groupadd --system linux-patch-api
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! getent passwd linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Creating user linux-patch-api..."
|
|
||||||
useradd --system \
|
|
||||||
--gid linux-patch-api \
|
|
||||||
--home-dir /var/lib/linux_patch_api \
|
|
||||||
--no-create-home \
|
|
||||||
--shell /usr/sbin/nologin \
|
|
||||||
--comment "Linux Patch API Service" \
|
|
||||||
linux-patch-api
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create required directories
|
# Create required directories
|
||||||
mkdir -p /etc/linux_patch_api/certs
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
mkdir -p /var/lib/linux_patch_api
|
mkdir -p /var/lib/linux_patch_api
|
||||||
mkdir -p /var/log/linux_patch_api
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set proper ownership
|
# Set proper ownership (service runs as root)
|
||||||
chown -R linux-patch-api:linux-patch-api /var/lib/linux_patch_api
|
chown -R root:root /var/lib/linux_patch_api
|
||||||
chown -R linux-patch-api:linux-patch-api /var/log/linux_patch_api
|
chown -R root:root /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set secure permissions
|
# Set secure permissions
|
||||||
chmod 750 /etc/linux_patch_api
|
chmod 750 /etc/linux_patch_api
|
||||||
|
|||||||
@ -5,7 +5,8 @@ After=network-online.target
|
|||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=notify
|
Type=simple
|
||||||
|
NotifyAccess=all
|
||||||
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
ExecStart=/usr/bin/linux-patch-api --config /etc/linux_patch_api/config.yaml
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
@ -16,12 +17,17 @@ RuntimeDirectory=linux-patch-api
|
|||||||
RuntimeDirectoryMode=0755
|
RuntimeDirectoryMode=0755
|
||||||
|
|
||||||
# Security hardening
|
# Security hardening
|
||||||
NoNewPrivileges=true
|
# NOTE: Package management requires extensive system access. The following
|
||||||
ProtectSystem=strict
|
# 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
|
ProtectHome=true
|
||||||
ReadWritePaths=/var/lib/linux_patch_api /var/log/linux_patch_api
|
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
PrivateDevices=true
|
|
||||||
ProtectHostname=true
|
ProtectHostname=true
|
||||||
ProtectClock=true
|
ProtectClock=true
|
||||||
ProtectKernelTunables=true
|
ProtectKernelTunables=true
|
||||||
@ -31,8 +37,6 @@ RestrictNamespaces=true
|
|||||||
LockPersonality=true
|
LockPersonality=true
|
||||||
MemoryDenyWriteExecute=false
|
MemoryDenyWriteExecute=false
|
||||||
RestrictRealtime=true
|
RestrictRealtime=true
|
||||||
RestrictSUIDSGID=true
|
|
||||||
RemoveIPC=true
|
|
||||||
|
|
||||||
# System call filtering (whitelist approach)
|
# System call filtering (whitelist approach)
|
||||||
SystemCallFilter=@system-service
|
SystemCallFilter=@system-service
|
||||||
@ -40,6 +44,7 @@ SystemCallErrorNumber=EPERM
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
Environment="RUST_BACKTRACE=1"
|
Environment="RUST_BACKTRACE=1"
|
||||||
|
Environment="DEBIAN_FRONTEND=noninteractive"
|
||||||
Environment="RUST_LOG=info"
|
Environment="RUST_LOG=info"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
|
|||||||
4
debian/postinst
vendored
4
debian/postinst
vendored
@ -13,14 +13,14 @@ if [ "$1" = "configure" ]; then
|
|||||||
echo "Creating default config.yaml..."
|
echo "Creating default config.yaml..."
|
||||||
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
cp /etc/linux_patch_api/config.yaml.example /etc/linux_patch_api/config.yaml
|
||||||
chmod 640 /etc/linux_patch_api/config.yaml
|
chmod 640 /etc/linux_patch_api/config.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/config.yaml
|
chown root:root /etc/linux_patch_api/config.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then
|
||||||
echo "Creating default whitelist.yaml..."
|
echo "Creating default whitelist.yaml..."
|
||||||
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
cp /etc/linux_patch_api/whitelist.yaml.example /etc/linux_patch_api/whitelist.yaml
|
||||||
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
chmod 640 /etc/linux_patch_api/whitelist.yaml
|
||||||
chown linux-patch-api:linux-patch-api /etc/linux_patch_api/whitelist.yaml
|
chown root:root /etc/linux_patch_api/whitelist.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Reload systemd daemon to pick up new service file
|
# Reload systemd daemon to pick up new service file
|
||||||
|
|||||||
12
debian/postrm
vendored
12
debian/postrm
vendored
@ -39,18 +39,6 @@ if [ "$1" = "purge" ]; then
|
|||||||
rm -rf /var/log/linux_patch_api
|
rm -rf /var/log/linux_patch_api
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Remove system user
|
|
||||||
if getent passwd linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Removing user linux-patch-api..."
|
|
||||||
userdel linux-patch-api 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Remove system group
|
|
||||||
if getent group linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Removing group linux-patch-api..."
|
|
||||||
groupdel linux-patch-api 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "linux-patch-api purged successfully"
|
echo "linux-patch-api purged successfully"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
23
debian/preinst
vendored
23
debian/preinst
vendored
@ -9,31 +9,14 @@ if [ -d "/etc/linux_patch_api" ]; then
|
|||||||
echo "Detected existing installation - performing upgrade"
|
echo "Detected existing installation - performing upgrade"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create system user if it doesn't exist
|
|
||||||
if ! getent group linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Creating group linux-patch-api..."
|
|
||||||
groupadd --system linux-patch-api
|
|
||||||
fi
|
|
||||||
|
|
||||||
if ! getent passwd linux-patch-api > /dev/null 2>&1; then
|
|
||||||
echo "Creating user linux-patch-api..."
|
|
||||||
useradd --system \
|
|
||||||
--gid linux-patch-api \
|
|
||||||
--home-dir /var/lib/linux_patch_api \
|
|
||||||
--no-create-home \
|
|
||||||
--shell /usr/sbin/nologin \
|
|
||||||
--comment "Linux Patch API Service" \
|
|
||||||
linux-patch-api
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Create required directories
|
# Create required directories
|
||||||
mkdir -p /etc/linux_patch_api/certs
|
mkdir -p /etc/linux_patch_api/certs
|
||||||
mkdir -p /var/lib/linux_patch_api
|
mkdir -p /var/lib/linux_patch_api
|
||||||
mkdir -p /var/log/linux_patch_api
|
mkdir -p /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set proper ownership
|
# Set proper ownership (service runs as root)
|
||||||
chown -R linux-patch-api:linux-patch-api /var/lib/linux_patch_api
|
chown -R root:root /var/lib/linux_patch_api
|
||||||
chown -R linux-patch-api:linux-patch-api /var/log/linux_patch_api
|
chown -R root:root /var/log/linux_patch_api
|
||||||
|
|
||||||
# Set secure permissions
|
# Set secure permissions
|
||||||
chmod 750 /etc/linux_patch_api
|
chmod 750 /etc/linux_patch_api
|
||||||
|
|||||||
@ -98,18 +98,9 @@ impl AptBackend {
|
|||||||
|
|
||||||
/// Run apt command and capture output
|
/// Run apt command and capture output
|
||||||
fn run_apt(&self, args: &[&str]) -> Result<String> {
|
fn run_apt(&self, args: &[&str]) -> Result<String> {
|
||||||
// Use sudo for operations that modify packages (install, upgrade, remove, purge)
|
// Service runs as root - no sudo needed for apt commands
|
||||||
let needs_sudo = args.first().is_some_and(|&cmd| {
|
let program = "apt";
|
||||||
matches!(
|
let cmd_args: Vec<&str> = args.to_vec();
|
||||||
cmd,
|
|
||||||
"install" | "upgrade" | "remove" | "purge" | "dist-upgrade" | "autoremove"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
let (program, cmd_args): (&str, Vec<&str>) = if needs_sudo {
|
|
||||||
("sudo", ["apt"].iter().chain(args.iter()).copied().collect())
|
|
||||||
} else {
|
|
||||||
("apt", args.to_vec())
|
|
||||||
};
|
|
||||||
|
|
||||||
let output = Command::new(program)
|
let output = Command::new(program)
|
||||||
.args(&cmd_args)
|
.args(&cmd_args)
|
||||||
|
|||||||
@ -47,3 +47,27 @@
|
|||||||
**Correction:** Add `sudo apt-get -f install -y` before `sudo apt-get install` in CI workflow to fix broken deps automatically.
|
**Correction:** Add `sudo apt-get -f install -y` before `sudo apt-get install` in CI workflow to fix broken deps automatically.
|
||||||
**Rule:** Always add `apt-get -f install -y` before `apt-get install` in CI workflows. Runners may have broken apt state from partial upgrades.
|
**Rule:** Always add `apt-get -f install -y` before `apt-get install` in CI workflows. Runners may have broken apt state from partial upgrades.
|
||||||
**Status:** Active
|
**Status:** Active
|
||||||
|
|
||||||
|
## 2026-05-03 - NoNewPrivileges=true blocks sudo in systemd services
|
||||||
|
**Mistake:** Service used NoNewPrivileges=true which prevented sudo from working (PERM_SUDOERS: setresuid Operation not permitted).
|
||||||
|
**Correction:** Removed NoNewPrivileges=true from systemd service. The service runs as root and uses sudo for apt commands, which requires privilege escalation capabilities.
|
||||||
|
**Rule:** For package management services that use sudo, do not use NoNewPrivileges=true. mTLS + IP whitelist provides network security.
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
## 2026-05-03 - RestrictSUIDSGID=true blocks sudo in systemd services
|
||||||
|
**Mistake:** Service used RestrictSUIDSGID=true which prevented sudo from using setuid/setgid operations.
|
||||||
|
**Correction:** Removed RestrictSUIDSGID=true from systemd service. Package management requires setuid/setgid for apt/dpkg.
|
||||||
|
**Rule:** For package management services, do not use RestrictSUIDSGID=true. It blocks sudo and apt from working.
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
## 2026-05-03 - dpkg preinst creates linux-patch-api user causing permission issues
|
||||||
|
**Mistake:** dpkg preinst script creates a linux-patch-api system user and changes directory ownership, causing the service to crash with 'Permission denied' on log file creation.
|
||||||
|
**Correction:** Fix dpkg preinst to not create the linux-patch-api user or change directory ownership. Service runs as root and directories should be owned by root.
|
||||||
|
**Rule:** For services that run as root, do not create a dedicated system user in the dpkg preinst script. Keep all directory ownership as root:root.
|
||||||
|
**Status:** Active
|
||||||
|
|
||||||
|
## 2026-05-03 - Service runs as root, no sudo needed for apt commands
|
||||||
|
**Mistake:** Service used sudo to run apt commands even though it runs as root. This caused failures when systemd security restrictions blocked sudo.
|
||||||
|
**Correction:** Removed sudo from apt command execution in the source code. Service runs as root and can execute apt directly.
|
||||||
|
**Rule:** If a service runs as root, it does not need sudo to execute commands. Remove sudo from command execution.
|
||||||
|
**Status:** Active
|
||||||
|
|||||||
@ -298,8 +298,8 @@ def test_get_package_not_found(client: PatchAPIClient) -> str:
|
|||||||
def test_install_package(client: PatchAPIClient) -> str:
|
def test_install_package(client: PatchAPIClient) -> str:
|
||||||
"""POST /api/v1/packages - Install a safe test package (hello).
|
"""POST /api/v1/packages - Install a safe test package (hello).
|
||||||
|
|
||||||
Note: Install may fail due to service permissions (NoNewPrivileges=true).
|
Verifies that the package installation completes successfully.
|
||||||
Both completed and failed are acceptable outcomes.
|
A failed status is a critical failure - the core function must work.
|
||||||
"""
|
"""
|
||||||
payload = {
|
payload = {
|
||||||
"packages": [{"name": TEST_PACKAGE, "version": None}],
|
"packages": [{"name": TEST_PACKAGE, "version": None}],
|
||||||
@ -318,10 +318,7 @@ def test_install_package(client: PatchAPIClient) -> str:
|
|||||||
# Poll job to completion
|
# Poll job to completion
|
||||||
job_id = data["data"]["job_id"]
|
job_id = data["data"]["job_id"]
|
||||||
job = poll_job(client, job_id)
|
job = poll_job(client, job_id)
|
||||||
# Install may fail due to service permissions - both outcomes acceptable
|
assert job["status"] == "completed", f"Install job failed: status={job['status']}, result={job.get('result', {})}"
|
||||||
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']}"
|
|
||||||
return f"Installed {TEST_PACKAGE}: job_id={job_id}, status={job['status']}"
|
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_id = data["data"]["job_id"]
|
||||||
job = poll_job(client, job_id)
|
job = poll_job(client, job_id)
|
||||||
# Update may complete or fail (package already latest or not installed)
|
assert job["status"] == "completed", f"Update job failed: status={job['status']}, result={job.get('result', {})}"
|
||||||
assert job["status"] in ["completed", "failed"], f"Unexpected job status: {job['status']}"
|
|
||||||
return f"Updated {TEST_PACKAGE}: job_id={job_id}, status={job['status']}"
|
return f"Updated {TEST_PACKAGE}: job_id={job_id}, status={job['status']}"
|
||||||
|
|
||||||
|
|
||||||
def test_remove_package(client: PatchAPIClient) -> str:
|
def test_remove_package(client: PatchAPIClient) -> str:
|
||||||
"""DELETE /api/v1/packages/{name} - Remove the test package.
|
"""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}")
|
resp = client.delete(f"/api/v1/packages/{TEST_PACKAGE}")
|
||||||
assert resp.status_code == 202, f"Expected 202, got {resp.status_code}: {resp.text}"
|
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_id = data["data"]["job_id"]
|
||||||
job = poll_job(client, job_id)
|
job = poll_job(client, job_id)
|
||||||
# Remove may fail if package wasn't installed
|
assert job["status"] == "completed", f"Remove job failed: status={job['status']}, result={job.get('result', {})}"
|
||||||
assert job["status"] in ["completed", "failed"], f"Remove job unexpected status: {job['status']}"
|
|
||||||
return f"Removed {TEST_PACKAGE}: job_id={job_id}, status={job['status']}"
|
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:
|
def test_job_lifecycle(client: PatchAPIClient) -> str:
|
||||||
"""Full job lifecycle: install -> get job -> list jobs -> remove.
|
"""Full job lifecycle: install -> get job -> list jobs -> remove.
|
||||||
|
|
||||||
Accepts both completed and failed outcomes for install/remove
|
Verifies that install and remove both complete successfully.
|
||||||
since service may have permission restrictions.
|
A failed status is a critical failure - the core function must work.
|
||||||
"""
|
"""
|
||||||
# Step 1: Install test package
|
# Step 1: Install test package
|
||||||
payload = {
|
payload = {
|
||||||
@ -589,7 +585,7 @@ def test_job_lifecycle(client: PatchAPIClient) -> str:
|
|||||||
|
|
||||||
# Step 3: Poll to completion
|
# Step 3: Poll to completion
|
||||||
job = poll_job(client, job_id)
|
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
|
# Step 4: Verify in job list
|
||||||
resp = client.get("/api/v1/jobs?limit=50", timeout=120)
|
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}"
|
assert resp.status_code == 202, f"Remove failed: HTTP {resp.status_code}"
|
||||||
remove_job_id = resp.json()["data"]["job_id"]
|
remove_job_id = resp.json()["data"]["job_id"]
|
||||||
remove_job = poll_job(client, remove_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}"
|
return f"Full lifecycle OK: install job={job_id}, remove job={remove_job_id}"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user