diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9561e87..5c933db 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,70 +26,66 @@ jobs: - name: Check formatting run: cargo fmt --all -- --check -# TEMPORARILY DISABLED - re-enable after build jobs are stable -# clippy: -# name: Clippy Lints -# runs-on: linux -# container: node:18 -# steps: -# - uses: actions/checkout@v2 -# with: -# fetch-depth: 0 -# - name: Install system dependencies -# run: | -# apt-get update -# apt-get install -y libsystemd-dev pkg-config -# - uses: dtolnay/rust-toolchain@stable -# with: -# components: clippy -# - name: Cache cargo -# uses: Swatinem/rust-cache@v2 -# - name: Run clippy -# run: cargo clippy --all-targets --all-features -- -D warnings -# -# test: -# name: Unit Tests -# runs-on: linux -# container: node:18 -# steps: -# - uses: actions/checkout@v2 -# with: -# fetch-depth: 0 -# - name: Install system dependencies -# run: | -# apt-get update -# apt-get install -y libsystemd-dev pkg-config -# - uses: dtolnay/rust-toolchain@stable -# - name: Cache cargo -# uses: Swatinem/rust-cache@v2 -# - name: Run tests -# run: cargo test --all-features -# - name: Upload coverage -# uses: codecov/codecov-action@v4 -# if: always() -# -# audit: -# name: Security Audit -# runs-on: linux -# container: node:18 -# steps: -# - uses: actions/checkout@v2 -# with: -# fetch-depth: 0 -# - name: Install system dependencies -# run: | -# apt-get update -# apt-get install -y libsystemd-dev pkg-config -# - uses: dtolnay/rust-toolchain@stable -# - name: Run cargo-audit -# run: | -# cargo install cargo-audit -# cargo audit - # Debian/Ubuntu Package Build + clippy: + name: Clippy Lints + runs-on: linux + container: node:18 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install system dependencies + run: | + apt-get update + apt-get install -y libsystemd-dev pkg-config + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + test: + name: Unit Tests + runs-on: linux + container: node:18 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install system dependencies + run: | + apt-get update + apt-get install -y libsystemd-dev pkg-config + - uses: dtolnay/rust-toolchain@stable + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --all-features + + audit: + name: Security Audit + runs-on: linux + container: node:18 + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install system dependencies + run: | + apt-get update + apt-get install -y libsystemd-dev pkg-config + - uses: dtolnay/rust-toolchain@stable + - name: Run cargo-audit + run: | + cargo install cargo-audit + cargo audit + build-deb: name: Build Debian Package runs-on: linux - container: debian:bookworm + container: node:18-bookworm steps: - uses: actions/checkout@v4 with: @@ -113,7 +109,7 @@ jobs: build-rpm: name: Build RPM Package runs-on: linux - container: fedora:latest + container: linux-patch-api-rpm:latest steps: - uses: actions/checkout@v4 with: @@ -121,7 +117,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Install RPM build tools run: | - dnf install -y rpm-build gcc cargo rust libsystemd-devel pkgconfig-pkg-config + dnf install -y rpm-build gcc cargo rust systemd-devel pkg-config - name: Build release binary run: cargo build --release - name: Build RPM package @@ -136,14 +132,22 @@ jobs: build-apk: name: Build Alpine Package runs-on: linux - container: alpine:latest + container: node:18-alpine steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Install Rust toolchain (rustup for edition2024 support) + run: | + apk add --no-cache curl + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable + source $HOME/.cargo/env + rustc --version + cargo --version - name: Install build dependencies run: | - apk add --no-cache rust cargo musl-dev openssl-dev systemd-dev git abuild + apk add --no-cache musl-dev openssl-dev git abuild gcc elogind-dev + # NOTE: abuild-keygen is now done inside build-alpine.sh to ensure keys persist in same shell session - name: Build APK package run: ./build-alpine.sh - name: Upload to releases (on tag) @@ -156,7 +160,7 @@ jobs: build-arch: name: Build Arch Package runs-on: linux - container: archlinux:latest + container: linux-patch-api-arch:latest steps: - uses: actions/checkout@v4 with: diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 8d19acf..247795e 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -2,7 +2,7 @@ ## System Overview -The Linux_Patch_API is a secure, single-host API service that enables remote package and patch management on Linux systems. Each instance runs as a systemd service on the managed host, providing a REST API over mTLS with strict IP whitelist enforcement. +The Linux_Patch_API is a secure, single-host API service that enables remote package and patch management on Linux systems. Each instance runs as a system service on the managed host (systemd on most distributions, OpenRC on Alpine), providing a REST API over mTLS with strict IP whitelist enforcement. **Architecture Type:** Agent Per Host (Option B) **Deployment:** One instance per managed Linux host @@ -45,8 +45,9 @@ The Linux_Patch_API is a secure, single-host API service that enables remote pac - Distribution detection and adapter selection 6. **Audit Logger** - - systemd journal integration (primary) - - Optional remote syslog server + - System logging integration (primary) + - systemd journal on systemd-based systems + - syslog/local files on OpenRC-based systems - Local file fallback (`/var/log/linux_patch_api/`) - 30-day retention with daily rotation and gzip compression @@ -59,9 +60,10 @@ The Linux_Patch_API is a secure, single-host API service that enables remote pac ### External Integrations - **Package Managers:** apt, dnf, yum, apk, pacman (via system commands) -- **systemd:** Service management and journal logging +- **Init System:** Service management and logging + - systemd (Debian, Ubuntu, RHEL, CentOS, Fedora) + - OpenRC (Alpine Linux) - **Internal CA:** Certificate validation against self-hosted CA -- **Remote Syslog:** Optional external log aggregation --- @@ -74,14 +76,17 @@ The Linux_Patch_API is a secure, single-host API service that enables remote pac - **mTLS:** Rust TLS library (rustls or native-tls) ### Infrastructure -- **Service Manager:** systemd +- **Service Manager:** Distribution-dependent + - systemd (most distributions) + - OpenRC (Alpine Linux) - **Configuration:** YAML -- **Logging:** systemd journal + optional syslog ### Deployment - **Package Format:** Native Linux packages (deb, rpm, apk, pkg.tar.zst) - **Distribution:** Via target system package manager (apt, dnf, apk, pacman) -- **Installation:** Package installs binary, systemd service, and default config structure +- **Installation:** Package installs binary, init script/service, and default config structure + - systemd unit file for systemd distributions + - OpenRC init script for Alpine - **Updates:** Handled through system package manager --- @@ -99,16 +104,21 @@ The Linux_Patch_API is a secure, single-host API service that enables remote pac - No granular permissions (binary access: allowed or denied) - Whitelisted IP + valid cert = full API access -### Process Security (systemd Hardening) +### Process Security (Init System Hardening) - **User:** root (required for package management) -- **NoNewPrivileges:** true (prevent privilege escalation) -- **ProtectSystem:** strict (read-only filesystem except allowed paths) -- **ProtectHome:** true (no access to /home, /root, /run/user) -- **PrivateTmp:** true (isolated /tmp) -- **SystemCallFilter:** Restrict to required syscalls only (application whitelist) -- **RestrictAddressFamilies:** AF_INET, AF_INET6, AF_UNIX (network restrictions) -- **CapabilityBoundingSet:** CAP_NET_BIND_SERVICE, CAP_SYS_ADMIN (minimal capabilities) +**systemd Hardening Options:** +- NoNewPrivileges: true (prevent privilege escalation) +- ProtectSystem: strict (read-only filesystem except allowed paths) +- ProtectHome: true (no access to /home, /root, /run/user) +- PrivateTmp: true (isolated /tmp) +- SystemCallFilter: Restrict to required syscalls only (application whitelist) + +**OpenRC Hardening Options:** +- Run as dedicated service user +- File permission restrictions +- chroot isolation (optional) +- Equivalent security via rc.conf and init script options ### Data Security - All communications encrypted via TLS - Certificates stored securely with restricted permissions @@ -149,7 +159,9 @@ The Linux_Patch_API is a secure, single-host API service that enables remote pac └── audit.log # Local audit log fallback /usr/bin/linux-patch-api # Binary location -/etc/systemd/system/linux-patch-api.service # Systemd service +Init scripts (distribution-dependent): +- /etc/systemd/system/linux-patch-api.service # systemd +- /etc/init.d/linux-patch-api # OpenRC (Alpine) ``` --- diff --git a/Dockerfile.arch b/Dockerfile.arch new file mode 100644 index 0000000..9b2419b --- /dev/null +++ b/Dockerfile.arch @@ -0,0 +1,13 @@ +# Arch Linux container with Node.js for GitHub Actions support +# Used for Arch package builds in CI/CD +FROM archlinux:latest + +# Update system and install Node.js (required for GitHub Actions JavaScript-based actions) +RUN pacman -Syu --noconfirm nodejs npm && \ + pacman -Scc --noconfirm + +# Verify node is available +RUN node --version + +# Default command (not used in CI, but good for testing) +CMD ["/bin/bash"] diff --git a/Dockerfile.rpm b/Dockerfile.rpm new file mode 100644 index 0000000..79d5c94 --- /dev/null +++ b/Dockerfile.rpm @@ -0,0 +1,14 @@ +# Fedora container with Node.js for GitHub Actions support +# Used for RPM package builds in CI/CD +FROM fedora:latest + +# Install Node.js (required for GitHub Actions JavaScript-based actions) +# Also install dnf-plugins-core for potential multiarch support +RUN dnf install -y nodejs dnf-plugins-core && \ + dnf clean all + +# Verify node is available +RUN node --version + +# Default command (not used in CI, but good for testing) +CMD ["/bin/bash"] diff --git a/SPEC.md b/SPEC.md index 955c526..f17e3b3 100644 --- a/SPEC.md +++ b/SPEC.md @@ -41,7 +41,9 @@ **Primary Objective:** Provide secure API for remote patch/package management on individual Linux hosts **Key Goals:** -- Run as systemd service on each managed machine (Option B: Agent Per Host) +- Run as a system service on each managed machine (Option B: Agent Per Host) + - systemd for Debian/Ubuntu, RHEL/CentOS/Fedora + - OpenRC for Alpine Linux - Internal network access only (no internet exposure) - Support Debian/Ubuntu first, then expand to other distributions - Maintain audit trail of all operations @@ -55,7 +57,9 @@ - One API instance per host - Internal network only (LAN/private network) - No public internet exposure -- Must run as systemd service +- Must run as a system service (init system determined by distribution) + - systemd: Debian, Ubuntu, RHEL, CentOS, Fedora + - OpenRC: Alpine Linux **Technical:** - Must run with elevated privileges for package management (root/sudo) @@ -119,7 +123,9 @@ ## Dependencies - Linux OS with package manager support -- systemd for service management +- Init system for service management (distribution-dependent) + - systemd (most distributions) + - OpenRC (Alpine Linux) - Network access for API communication - mTLS certificate infrastructure (CA, client certs) - IP whitelist configuration @@ -147,8 +153,10 @@ - Configuration changes (whitelist updates, cert renewals) - **Log Storage:** - - Primary: systemd journal (`journalctl`) - - Secondary: Optional remote syslog server + - Primary: Distribution-appropriate logging + - systemd journal (journalctl) on systemd systems + - syslog/local files on OpenRC systems + - Secondary: Optional remote syslog server (universal) - Local file logs as fallback (`/var/log/linux_patch_api/`) - **Log Retention:** diff --git a/build-alpine.sh b/build-alpine.sh index c04f4d0..4740e06 100755 --- a/build-alpine.sh +++ b/build-alpine.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/sh # Build Alpine Package (.apk) # Run on: Alpine Linux 3.18+ # Or in Docker: docker run -v $(pwd):/build alpine:latest /build/build-alpine.sh @@ -8,12 +8,34 @@ set -e echo "=== Linux Patch API - Alpine Build Script ===" echo "" +# Source cargo environment (for rustup-installed toolchain in CI) +if [ -f "$HOME/.cargo/env" ]; then + . "$HOME/.cargo/env" +fi + +# Check if running on Alpine + +# Check if running on Alpine # Check if running on Alpine if ! command -v abuild &> /dev/null; then echo "Installing Alpine build tools..." - apk add --no-cache alpine-sdk rust cargo openssl-dev systemd-dev git + apk add --no-cache alpine-sdk rust cargo openssl-dev openrc git fi +# Generate abuild signing keys (ALWAYS generate fresh - same shell session as abuild commands) +echo "Generating abuild signing keys..." +apk add --no-cache abuild +abuild-keygen -a -n 2>&1 | tee /tmp/keygen.log +# Find the actual key file (handles missing username prefix) +KEYFILE=$(ls /root/.abuild/*.rsa 2>/dev/null | head -1) +if [ -z "$KEYFILE" ]; then + KEYFILE=$(ls /root/.abuild/-*.rsa 2>/dev/null | head -1) +fi +echo "Found key: $KEYFILE" +# Write directly to abuild.conf (overwrite any stale config) +echo "PACKAGER_PRIVKEY=\"$KEYFILE\"" > /etc/abuild.conf +cat /etc/abuild.conf + # Setup build environment echo "Setting up build environment..." export CBUILDROOT=$(pwd)/.abuild @@ -27,13 +49,13 @@ cargo build --release --target x86_64-unknown-linux-musl PKGDIR=$(pwd)/apk-package mkdir -p "$PKGDIR"/usr/bin mkdir -p "$PKGDIR"/etc/linux_patch_api -mkdir -p "$PKGDIR"/lib/systemd/system +mkdir -p "$PKGDIR"/etc/init.d # Copy files cp target/x86_64-unknown-linux-musl/release/linux-patch-api "$PKGDIR"/usr/bin/ chmod 755 "$PKGDIR"/usr/bin/linux-patch-api -cp configs/linux-patch-api.service "$PKGDIR"/lib/systemd/system/ -cp configs/config.yaml.example "$PKGDIR"/etc/linux_patch_api/config.yaml +cp configs/linux-patch-api-openrc "$PKGDIR"/etc/init.d/linux-patch-api +chmod 755 "$PKGDIR"/etc/init.d/linux-patch-api cp configs/whitelist.yaml.example "$PKGDIR"/etc/linux_patch_api/whitelist.yaml # Create APKBUILD @@ -46,17 +68,60 @@ pkgdesc="Secure remote package management API for Linux systems" url="https://gitea.internal/linux-patch-api" arch="x86_64" license="MIT" -depends="systemd" -source="apk-package" +makedepends="" +depends="openrc" +source="" package() { - cp -r "$srcdir"/apk-package/* "$pkgdir"/ + # Create directory structure in pkgdir + install -d "$pkgdir"/usr/bin + install -d "$pkgdir"/etc/linux_patch_api + install -d "$pkgdir"/etc/init.d + # Copy from pre-built apk-package directory + cp -r /workspace/echo/linux_patch_api/apk-package/usr/bin/* "$pkgdir"/usr/bin/ + cp -r /workspace/echo/linux_patch_api/apk-package/etc/linux_patch_api/* "$pkgdir"/etc/linux_patch_api/ + cp -r /workspace/echo/linux_patch_api/apk-package/etc/init.d/* "$pkgdir"/etc/init.d/ } EOF +# Generate checksums for APKBUILD sources +echo "Generating checksums..." + # Build APK package echo "Building APK package..." -abuild -F -r + +# For CI/container environments where we run as root, create a build user +if [ "$(id -u)" = "0" ]; then + echo "Running as root - creating build user for abuild..." + adduser -D -s /bin/sh builduser 2>/dev/null || true + # CRITICAL: Add builduser to abuild group (required for apk install permissions) + addgroup builduser abuild 2>/dev/null || usermod -aG abuild builduser + chown -R builduser:builduser "$(pwd)" + chown -R builduser:builduser /root/packages 2>/dev/null || true + # Copy abuild keys from root to builduser home + mkdir -p /home/builduser/.abuild + cp /root/.abuild/* /home/builduser/.abuild/ + chown -R builduser:builduser /home/builduser/.abuild + + # Find the actual key file + KEYFILE=$(ls /home/builduser/.abuild/*.rsa 2>/dev/null | head -1) + if [ -z "$KEYFILE" ]; then + KEYFILE=$(ls /home/builduser/.abuild/-*.rsa 2>/dev/null | head -1) + fi + + echo "Key file: $KEYFILE" + echo "Key file exists: $(test -f "$KEYFILE" && echo YES || echo NO)" + + # CRITICAL: Write to builduser's PERSONAL abuild.conf (~/.abuild/abuild.conf) + # abuild reads this when running as builduser - standard behavior, no shell quoting issues! + echo "PACKAGER_PRIVKEY=\"$KEYFILE\"" > /home/builduser/.abuild/abuild.conf + chown builduser:builduser /home/builduser/.abuild/abuild.conf + su - builduser -c "cd $(pwd) && abuild checksum && abuild -d -F && cp /home/builduser/packages/x86_64/*.apk ./releases/ 2>/dev/null || cp /home/builduser/packages/*.apk ./releases/ 2>/dev/null || ls -la /home/builduser/packages/" +else + abuild checksum + abuild -F -r + cp ~/packages/x86_64/*.apk releases/ 2>/dev/null || cp ~/packages/*.apk releases/ 2>/dev/null || true +fi # Copy to releases directory echo "" diff --git a/build-arch.sh b/build-arch.sh index b2e2b95..f4fca2c 100755 --- a/build-arch.sh +++ b/build-arch.sh @@ -43,24 +43,34 @@ pkgname=linux-patch-api pkgver=1.0.0 pkgrel=1 pkgdesc="Secure remote package management API for Linux systems" -url="https://gitea.internal/linux-patch-api" +url="https://gitea.moon-dragon.us/echo/linux_patch_api" arch=('x86_64') license=('MIT') depends=('systemd') -source=('arch-package') package() { - cp -r "$srcdir"/arch-package/* "$pkgdir"/ + # Use absolute path since makepkg changes working directory to srcdir + cp -r /workspace/echo/linux_patch_api/arch-package/* "$pkgdir"/ } EOF # Create .SRCINFO echo "Creating .SRCINFO..." -makepkg --printsrcinfo > .SRCINFO # Build package echo "Building Arch package..." -makepkg -f --noconfirm + +# For CI/container environments where we run as root, create a build user +if [ "$(id -u)" = "0" ]; then + echo "Running as root - creating build user for makepkg..." + useradd -m builduser 2>/dev/null || true + chown -R builduser:builduser "$(pwd)" + su - builduser -c "cd $(pwd) && makepkg --printsrcinfo > .SRCINFO" + su - builduser -c "cd $(pwd) && makepkg -f --noconfirm" +else + makepkg --printsrcinfo > .SRCINFO + makepkg -f --noconfirm +fi # Copy to releases directory echo "" diff --git a/configs/linux-patch-api-openrc b/configs/linux-patch-api-openrc new file mode 100644 index 0000000..2aee4cf --- /dev/null +++ b/configs/linux-patch-api-openrc @@ -0,0 +1,72 @@ +#!/sbin/openrc-run +# OpenRC init script for linux-patch-api +# Used on Alpine Linux and other OpenRC-based systems + +name="linux_patch_api" +command="/usr/bin/linux-patch-api" +command_args="--config /etc/linux_patch_api/config.yaml" +command_background=true +pidfile="/run/linux-patch-api/linux-patch-api.pid" +output_log="/var/log/linux_patch_api/linux-patch-api.log" +error_log="/var/log/linux_patch_api/linux-patch-api.err" + +# Required dependencies +depend() { + use net logger +} + +# Create required directories before starting +start_pre() { + checkpath --directory --owner linux-patch-api:linux-patch-api --mode 0755 \ + /run/linux-patch-api \ + /var/log/linux-patch-api \ + /var/lib/linux-patch-api \ + /etc/linux_patch_api/certs + + # Ensure config files exist + if [ ! -f "/etc/linux_patch_api/config.yaml" ]; then + eerror "Configuration file missing: /etc/linux_patch_api/config.yaml" + eerror "Please create config.yaml before starting the service" + return 1 + fi + + if [ ! -f "/etc/linux_patch_api/whitelist.yaml" ]; then + eerror "Whitelist file missing: /etc/linux_patch_api/whitelist.yaml" + eerror "Please create whitelist.yaml before starting the service" + return 1 + fi +} + +# Verify service started successfully +start_post() { + sleep 2 + if [ -f "$pidfile" ]; then + einfo "linux-patch-api started successfully (PID: $(cat $pidfile))" + else + ewarn "linux-patch-api may not have started correctly - pidfile not found" + fi +} + +# Clean shutdown +stop_pre() { + einfo "Stopping linux-patch-api service..." +} + +# Verify service stopped +stop_post() { + if [ -f "$pidfile" ]; then + rm -f "$pidfile" + fi + einfo "linux-patch-api stopped" +} + +# Service status +status() { + if [ -f "$pidfile" ] && kill -0 $(cat "$pidfile") 2>/dev/null; then + einfo "linux-patch-api is running (PID: $(cat $pidfile))" + return 0 + else + eerror "linux-patch-api is not running" + return 1 + fi +} diff --git a/linux-patch-api.spec b/linux-patch-api.spec index 3c4c1e8..691bfa4 100644 --- a/linux-patch-api.spec +++ b/linux-patch-api.spec @@ -1,3 +1,5 @@ +%global debug_package %{nil} + Name: linux-patch-api Version: 1.0.0 Release: 1%{?dist}