CI Pipeline / Rust Format Check (push) Successful in 10s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Failing after 1m32s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
Root cause: postinst ran sqlx migrate as postgres (superuser), creating ALL
database objects owned by postgres. When pm-web connects as patch_manager, it
cannot ALTER TABLE during migrations because it does not own them. The
reassign_ownership() function never worked because REASSIGN OWNED BY postgres
TO patch_manager fails for superuser-owned objects.
Fix: Create the database owned by patch_manager (already done) and run all
migrations as patch_manager via PGPASSWORD auth. When all objects are owned by
patch_manager from the start, pm-web can ALTER them during upgrades.
Changes:
- Add psql_run_as_pm() helper that authenticates as patch_manager via PGPASSWORD
- Replace all psql_run_db calls in apply_migrations() with psql_run_as_pm
- Remove reassign_ownership() function entirely (it never worked)
- Remove reassign_ownership call from main()
- Add ALTER DEFAULT PRIVILEGES FOR ROLE postgres in setup_database() as safety
net for any future migration that might run as postgres
- Upgrade GRANT USAGE/CREATE to GRANT ALL PRIVILEGES on schema public
- Keep pgcrypto extension creation as postgres (requires superuser)
- Renumber sections after removing reassign_ownership
Proven on live LPM system: service active, port 443 listening, all tables
owned by patch_manager.
The postinst script runs migrations as the postgres superuser, which
means all created tables, enum types, and sequences are owned by
postgres. When pm-web connects as patch_manager and tries to ALTER
tables during upgrades, it fails with 'must be owner of table groups'.
Add reassign_ownership() function that runs after apply_migrations()
and before systemctl start. This function:
- REASSIGN OWNED BY postgres TO patch_manager (tables, types, sequences)
- ALTER SCHEMA public OWNER TO patch_manager
- GRANT ALL PRIVILEGES on database, schema, tables, sequences, functions
- ALTER DEFAULT PRIVILEGES for future objects in public schema
Renumbered sections 6-10 to 6-12 to accommodate the new function.
- write_config(): Replace CHANGEME placeholder on upgrade instead of
skipping entirely; preserve existing real passwords unchanged
- setup_database(): When DB user already exists, recover password from
existing config and sync to PostgreSQL, or generate a fresh password;
fixes crash-loop when config password diverges from PostgreSQL
- generate_jwt_keys(): Regenerate missing verify.pem from existing
signing.pem instead of silently skipping
- Password extraction uses @localhost anchor to correctly handle
passwords containing @ characters
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Failing after 1m46s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
Three issues fixed in the multi-stage Docker build:
1. CRITICAL: Add COPY migrations/ to rust-builder stage
- sqlx::migrate!(../../migrations) is a compile-time proc macro
- Without migrations/ present, cargo build fails with 'no such file or directory'
- Previously migrations/ was only copied in runtime stage (too late)
2. Copy individual crate Cargo.toml files for dependency caching
- The dummy-build caching step only copied workspace Cargo.toml/Cargo.lock
- Without crate-level manifests, cargo couldn't resolve the workspace
- This meant the cache layer was ineffective (rebuilt everything on code changes)
3. Add openssl package to runtime stage
- entrypoint.sh uses openssl rand, openssl genpkey, openssl pkey
- Only libssl3t64 (shared library) was installed, not the CLI tool
- Runtime would fail on first-run key generation
All stages verified: Ubuntu 24.04 ✅ Rust via rustup (1.85+) ✅
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Failing after 1m21s
CI Pipeline / Security Audit (push) Successful in 6s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 16s
CI Pipeline / Build .deb & Release (push) Has been skipped
- Stage 1 (Rust): Replace rust:1.85-bookworm with ubuntu:24.04 + rustup stable
- Stage 2 (Frontend): Replace node:20-bookworm-slim with ubuntu:24.04 + NodeSource Node.js 20
- Stage 3 (Runtime): Already ubuntu:24.04 with libssl3t64 (verified correct)
- docker-compose: Change postgres:16-bookworm to postgres:16 (standard image)
This aligns Docker builds with the project's target OS (Ubuntu 24.04) and
matches the CI environment which runs on ubuntu-latest (24.04).
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Failing after 1m54s
CI Pipeline / Security Audit (push) Successful in 7s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 17s
CI Pipeline / Build .deb & Release (push) Has been skipped
* fix(docker): add PostgreSQL APT repo for postgresql-client-16
Debian Bookworm default repos only ship PostgreSQL 15. The Docker
runtime stage needs postgresql-client-16 for the entrypoint script,
so add the official PGDG APT repository.
- Add PGDG GPG key and sources.list entry for bookworm-pgdg
- Install ca-certificates and curl first (needed for repo setup)
- Purge gnupg2 after use to keep image lean
- Verify argon2 package name is correct for Bookworm (it is)
* fix(docker): use ubuntu:24.04 runtime instead of debian:bookworm-slim
The project targets Ubuntu 24.04, not Debian Bookworm. Ubuntu 24.04
includes PostgreSQL 16 in default repos, eliminating the need for the
PGDG APT repo workaround. Also fixes libssl3 → libssl3t64 package name
for the time64 transition in Ubuntu 24.04.
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 51s
CI Pipeline / Rust Unit Tests (push) Failing after 1m31s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 14s
CI Pipeline / Build .deb & Release (push) Has been skipped
- Remove all cert files from git tracking (git rm --cached)
- crates/pm-agent-client/certs/client.key (private key)
- crates/pm-agent-client/certs/client.crt (public cert)
- crates/pm-agent-client/certs/ca.crt (public cert)
- Add .gitignore patterns for *.key, *.key.pem, certs/*.crt, certs/*.pem
- Update pm-agent-client doc examples to use std::fs::read() instead of include_bytes!
- Add gitleaks secret scanning job to CI workflow
- Update security-review.md with critical finding for Issue #12
- Add README.md to crates/pm-agent-client/certs/ explaining runtime cert generation
Private keys were dev/test only - no production key rotation needed.
Git history purge with filter-repo will follow after PR merge.
Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
CI Pipeline / Rust Format Check (push) Successful in 7s
CI Pipeline / Clippy Lints (push) Successful in 50s
CI Pipeline / Rust Unit Tests (push) Successful in 1m9s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
The generate_crl() SQL query referenced a non-existent column
"not_after" instead of the actual column "expires_at" in the
certificates table. This caused a 500 error when requesting the CRL
endpoint because PostgreSQL could not find the column.
Fixes: CRL endpoint returns 500 Internal Server Error
Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
* feat: add CRL health status schema and UI (PR 3 of 6)
* fix(lint): strict equality for crl_age_seconds
---------
Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
CI Pipeline / Rust Format Check (push) Successful in 6s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m10s
CI Pipeline / Security Audit (push) Successful in 1m26s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
* feat(pki): add CRL generation, distribution endpoint, and enrollment bundle extension
Implements manager-side CRL infrastructure for issue #7:
- Add CertAuthority::generate_crl() using rcgen 0.13
- Add GET /api/v1/pki/crl.pem public endpoint
- Extend PkiBundle with ca_chain and crl_pem fields
- Update enrollment route to include CRL in bundle
- Mount pki route as public endpoint
- Add proptest dev-dependency
* style: fix cargo fmt in enrollment.rs
---------
Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 50s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 4s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 14s
CI Pipeline / Build .deb & Release (push) Has been skipped
* feat(security): replace hardcoded admin password with in-app bootstrap (issue #8)
Replace the publicly-known Argon2id hash in 002_seed_admin.sql with a
clearly-invalid placeholder that cannot validate any password (fail-closed).
On first startup, pm-web detects the placeholder and generates a random
24-character alphanumeric password, hashes it with Argon2id, and UPDATEs
the admin row. The plaintext password is printed once to stderr (visible
in systemd journal).
This eliminates the need for a separate hash_password binary, shell
script SQL injection risk, and password leakage in shell variables.
Closes#8
* fix(security): rustfmt compliance for bootstrap function
* fix(security): add trailing commas to match arms per rustfmt
CI Pipeline / Rust Format Check (push) Successful in 8s
CI Pipeline / Clippy Lints (push) Successful in 50s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
Encrypt three sensitive secrets that were stored in plaintext: OIDC client_secret, SMTP smtp_password, TOTP totp_secret. AES-256-GCM via pm-core::crypto helper. New per-install key at /etc/patch-manager/keys/secret-encryption.key, separate from health-check.key for blast-radius isolation. MASKED placeholder behavior in API responses is preserved.
23 files changed, +1248 / -28. Closes#6.
Replaces URL-embedded JWT tokens with a single-use, 60-second handoff code that the SPA exchanges via server-to-server POST. The URL now contains only `?handoff=<code>` — no tokens are placed in the browser history, proxy access logs, or Referer header.
Backend: new SsoHandoff store (DashMap, 60s TTL, atomic DashMap::remove for single-use), POST /api/v1/auth/sso/handoff endpoint, 7 new tests.
Frontend: SsoCallbackPage rewritten to use useSearchParams + POST exchange, with history.replaceState to clear the handoff code from the address bar. Switched from window.location.search to useSearchParams() for test compatibility. New Vitest infrastructure (vitest, @testing-library/react, jsdom) and 6 new tests.
CI fix in ccba9e3: cargo fmt --all and added searchParams to useEffect dep array to satisfy CI's Rust Format and Frontend Lint checks.
Refs: closes#4
Hardens the IP allowlist in require_auth against the two bypasses filed in #3.
1. Bypass via missing X-Forwarded-For (no IP to check, allowlist skipped).
2. Spoofing via attacker-controlled X-Forwarded-For (header trusted unconditionally).
Resolves both by deriving the client IP from the socket peer (ConnectInfo<SocketAddr>) and only honoring X-Forwarded-For when the immediate peer is in a new security.trusted_proxies allowlist (default empty = strict). Fails closed with 403 forbidden_ip when a non-empty allowlist is configured and the client IP cannot be determined. Empty ip_whitelist continues to mean allow all (preserved for dev installs).
27 pm-auth tests pass (12 new resolver + 8 new middleware + 7 existing). Spec: tasks/ip-allowlist-spec.md.
- Add single-retrieval semantics: approved PKI bundles are atomically
removed from the in-memory cache on first retrieval via DashMap::remove(),
preventing concurrent requests from obtaining the private key
- Add TTL expiry: ApprovedEntry wraps PkiBundle with approved_at and ttl
fields; bundles expire after ENROLLMENT_BUNDLE_TTL_SECS (600s / 10 min)
- Replace brute-force clear() purge with TTL-based retain() in background
task, running every 60s instead of every 600s
- Audit tracing calls: confirm no raw polling token is logged; add security
comment documenting this policy
- Document CSR-based enrollment as future enhancement in both enrollment.rs
and SECURITY.md, explaining why server-generated keys are used currently