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
ClosesDraco-Lunaris/Linux-Patch-Manager#10
The browser WebSocket endpoint at GET /api/v1/ws/jobs previously
authenticated solely via a single-use, 60-second ticket passed as a query
parameter. A leaked ticket (browser history, Referer, proxy logs, support
bundles) could be redeemed from any origin, enabling Cross-Site WebSocket
Hijacking (CSWSH).
This change adds a second gate: the Origin header must match an explicit
allowlist. The check runs BEFORE ticket validation so that rejected
cross-origin probes do not consume the legitimate users ticket.
Changes:
- pm-core: new security.allowed_origins config field; default derived
from sso_callback_url; startup warning if both are unparseable
- pm-web: ws_handler extracts HeaderMap and calls check_origin first;
returns 403 on missing/malformed/disallowed origins
- config: documented allowed_origins key in config.example.toml
- docs: security-review.md section 1.4 (WebSocket Origin Allowlist)
- tests: 40 unit tests (7 pm-core, 33 pm-web)
CI Pipeline / Rust Format Check (pull_request) Failing after 3s
CI Pipeline / Clippy Lints (pull_request) Failing after 1s
CI Pipeline / Rust Unit Tests (pull_request) Failing after 2s
CI Pipeline / Security Audit (pull_request) Failing after 1s
CI Pipeline / Frontend Lint & Type Check (pull_request) Failing after 4s
CI Pipeline / Build .deb & Release (pull_request) Has been skipped
- Add GET /api/v1/maintenance-windows bulk endpoint to eliminate N+1
per-host API calls (1 request instead of N+1)
- Fix two-phase state update race: setHosts() was called before
setWindowsByHost(), causing React to render hosts with empty windows
- Add AbortController to cancel stale fetch requests on unmount/re-fetch
- Batch state updates atomically (React 18 auto-batching)
- Replace silent catch{} with proper error handling
- Add refreshData() wrapper for mutation handlers and Refresh button
Backend: maintenance_windows.rs - new list_all_windows handler +
all_windows_router(), mounted in main.rs
Frontend: client.ts - new listAll() API method
Frontend: MaintenanceWindowsPage.tsx - rewritten fetchData
- Replace custom DashMap<IpAddr, Instant> rate limiting in enrollment.rs
that fell back to 0.0.0.0 when X-Forwarded-For was missing, causing
ALL enrollment traffic to share a single global rate limit bucket
- Use tower_governor with SmartIpKeyExtractor for proper per-IP rate
limiting that respects X-Forwarded-For headers (critical behind HAProxy)
- Add three configurable rate limit tiers via config.toml:
* Enrollment: 5 req/min per IP, burst 3 (strict)
* Auth: 20 req/min per IP, burst 10 (moderate)
* API: 120 req/min per IP, burst 30 (normal)
- Remove enrollment_rate_limits from AppState and cleanup task
- Remove manual rate limit code from enrollment.rs (headers param, IP extraction)
- Add into_make_service_with_connect_info for ConnectInfo fallback
- Add RateLimitConfig to AppConfig with sensible defaults
Fixes: #1
CI Pipeline / Rust Format Check (push) Failing after 2s
CI Pipeline / Clippy Lints (push) Failing after 1s
CI Pipeline / Rust Unit Tests (push) Failing after 2s
CI Pipeline / Security Audit (push) Failing after 2s
CI Pipeline / Frontend Lint & Type Check (push) Failing after 3s
CI Pipeline / Build .deb & Release (push) Has been skipped
- health_poller: persist agent_version from HealthData.version
- health_poller: call /system/info to update os_family, os_name, arch
- enrollment: set os_family and arch from os_details during approval
- enrollment: build os_name from os+os_version when name field absent
- COALESCE in UPDATE preserves existing values when new data unavailable
- version bump 0.1.7 -> 0.1.8
- main.rs: use config.security.ca_cert_path parent directory instead
of hardcoded /etc/patch-manager/ca for CA initialization.
- config.example.toml: add warning that CA key must be unencrypted PEM.
- This prevents silent generation of a second CA on fresh installs
and ensures the manager always uses the configured CA.
When a user already exists with auth_provider=local and the same email,
the SSO callback now links the existing user to the new SSO provider
instead of failing with a unique constraint violation on email.
Flow: 1) Try exact match (email+auth_provider), 2) If not found,
try email-only lookup, 3) If found with different provider, link
by updating auth_provider, azure_oid, oidc_sub, 4) If not found,
create new user as before.
The SSO button on the login page was not appearing because the settings
API requires authentication, but the login page cannot authenticate before
the user logs in.
Changes:
- Backend: Add GET /api/v1/auth/sso/config public endpoint that returns
only enabled, display_name, and auth_url (no secrets exposed)
- Backend: Mount sso::public_router() at /api/v1/auth/sso in main.rs
(was previously missing - only azure_compat_router was mounted)
- Frontend: Replace settingsApi.get() call in LoginPage.tsx with
ssoConfigApi.get() which calls the public endpoint
- Frontend: Add SsoConfigResponse interface and ssoConfigApi helper
to client.ts
- Frontend: Use auth_url from config response instead of hardcoded path
- Refactored azure_sso.rs to sso.rs with generic OIDC provider support
- Added OIDC discovery URL lookup with 1hr TTL caching
- Added PKCE for all providers, client_secret optional for public clients
- Added /api/v1/auth/sso/login and /api/v1/auth/sso/callback routes
- Added /api/v1/auth/azure/* backward-compatible routes
- Added POST /settings/sso/discover and POST /settings/sso/test endpoints
- Frontend: Provider dropdown (Keycloak/Azure AD/Custom OIDC)
- Frontend: Auto-fill discovery URL for Keycloak
- Frontend: Discover Endpoints and Test Connection buttons
- Frontend: Dynamic SSO button based on provider display name
- Made migration 014 idempotent with DO blocks and IF NOT EXISTS
- Fixed debian/install to use /usr/local/bin/ for binaries
- Fixed frontend file path in .deb package
- Reset admin password on dev server
- Fixed database permissions for oidc_config table
1. Compliance CSV/PDF: Replace non-existent pd.total_packages with
jsonb_array_length(pd.installed_packages) and pd.pending_patches
with pd.patch_count. Fix GROUP BY to match new columns.
2. Vulnerability CSV/PDF: Replace non-existent pd.cve_data with
jsonb_array_elements on pd.available_patches JSONB, extracting
cve_ids via nested lateral join. Replace pd.updated_at with
pd.polled_at (actual column name).
3. TypeScript: Remove duplicate PollingConfig interface declaration
in frontend/src/types/index.ts.
4. ReportsPage: Replace Group ID text field with Select dropdown
populated from GET /api/v1/groups, showing group names instead
of requiring UUID input.
5. PDF charts: Increase embed_image scale from 0.18 to 0.28 for
better visibility on A4 landscape pages.
6. Vulnerability CSV: Remove invalid (no data) comment row on
query failure; return header-only CSV instead to maintain valid
CSV format.
- Add target_host_id column to host_health_checks table (nullable UUID FK)
- Allow service checks to query a different host agent
- Backend models, API routes, and poller updated
- Frontend: host selector dropdown for service checks
- Validation: target host must exist and be healthy
- FK ON DELETE SET NULL: revert to own host if target deleted