33 Commits
Author SHA1 Message Date
me0nline f5ff8ab6cc ci(windows): use native PowerShell git checkout to eliminate Node.js dependency on Windows runner
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 1m45s
Release Binaries & Installers / Build & Release Windows Binaries & Installers (push) Failing after 16s
Release Binaries & Installers / Build & Release Linux Binaries (push) Successful in 2m8s
2026-09-04 23:54:13 +02:00
me0nline fd6da560ed docs: update release tag instructions for v2.0.1 in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m15s
2026-09-04 23:31:44 +02:00
me0nline 956dfc5d93 docs(release): publish release notes for LOGAR v2.0.1 2026-09-04 23:31:29 +02:00
me0nline 35e6a8df3e ci: bump default release tag to v2.0.1 in release.yml 2026-09-04 23:31:15 +02:00
me0nline c121291dda build(installer): bump server AppVersion to 2.0.1 in installer_server.iss 2026-09-04 23:31:03 +02:00
me0nline d19bbb2ec1 build(installer): bump client AppVersion to 2.0.1 in installer_client.iss 2026-09-04 23:30:50 +02:00
me0nline e6dc82ff55 chore(server): bump server version to 2.0.1 2026-09-04 23:30:36 +02:00
me0nline 56d8516427 docs: update README.md to reference consolidated release.yml workflow
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m24s
2026-09-04 23:26:05 +02:00
me0nline 94ad3f9461 ci: remove obsolete release-windows.yml in favor of consolidated release.yml 2026-09-04 23:25:38 +02:00
me0nline 26a4509429 ci: remove obsolete release-linux.yml in favor of consolidated release.yml 2026-09-04 23:25:35 +02:00
me0nline 80ae42088f ci: add consolidated release.yml combining Linux and Windows release jobs 2026-09-04 23:25:32 +02:00
me0nline 42f01addca docs: document 25 unit tests and tripartite release options in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
2026-09-04 23:23:08 +02:00
me0nline a9f096ef1b ci(windows): optimize tag triggers and add default v2.0.0 tag to release-windows.yml 2026-09-04 23:22:52 +02:00
me0nline 26d3d59812 ci(linux): optimize tag triggers and add default v2.0.0 tag to release-linux.yml 2026-09-04 23:22:38 +02:00
me0nline 5fec32327f ci: prevent ci.yml from triggering on tag pushes
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m22s
2026-09-04 23:08:19 +02:00
me0nline 478807d873 ci(linux): add v* tag pattern to release-linux.yml 2026-09-04 23:08:06 +02:00
me0nline 149ba6dfec ci(windows): add release event and multi-pattern tag triggers to release-windows.yml 2026-09-04 23:07:54 +02:00
me0nline a11b05f0a9 docs: update release tag instructions for v2.0.0 in root README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Successful in 2m21s
2026-09-04 22:57:27 +02:00
me0nline 722d2a1fec docs(release): publish release notes for LOGAR v2.0.0 2026-09-04 22:57:17 +02:00
me0nline 5883b78822 build(installer): bump server AppVersion to 2.0.0 in installer_server.iss 2026-09-04 22:57:02 +02:00
me0nline d79de301bf build(installer): bump client AppVersion to 2.0.0 in installer_client.iss 2026-09-04 22:56:53 +02:00
me0nline 184fdc6bc6 docs: add certificate auto-renewal details to out/linux_client/README.md
CI Test Suite / Run Component Tests & Pipeline Verification (push) Has been cancelled
2026-09-04 22:55:09 +02:00
me0nline 4625b650e5 docs: add certificate auto-renewal details to out/win_client/README.md 2026-09-04 22:54:59 +02:00
me0nline f38bbfb540 docs: add certificate watchdog details to out/linux_server/README.md 2026-09-04 22:54:47 +02:00
me0nline 18f0286692 docs: add certificate watchdog details to out/win_server/README.md 2026-09-04 22:54:36 +02:00
me0nline c9f769ef2b docs: document certificate validity watchdog and auto-renewal in root README.md 2026-09-04 22:54:25 +02:00
me0nline c01c09ecbd test(linux_client): add test_client_certificate_validity_and_proactive_check in test_linux_client.py 2026-09-04 22:52:49 +02:00
me0nline 2afe94fb36 test(win_client): add test_client_certificate_validity_and_proactive_check in test_win_client.py 2026-09-04 22:52:34 +02:00
me0nline 4650cbcafc test(server): add test_cert_validity_and_hub_pki_renewal in test_server.py 2026-09-04 22:52:18 +02:00
me0nline 0d613b3d22 feat(linux_client): add proactive certificate expiry check and reactive self-healing in Linux_Client.py 2026-09-04 22:52:00 +02:00
me0nline 916d3764a2 feat(win_client): add proactive certificate expiry check and reactive self-healing in Win_Client.py 2026-09-04 22:51:28 +02:00
me0nline 0e7d299594 feat(server): add in-flight certificate validity watchdog and dynamic SSLContext reloading in Server.py 2026-09-04 22:50:50 +02:00
me0nline 2cff629e23 feat(pki): add certificate expiration check and auto-renewal in server_enrollment.py 2026-09-04 22:49:54 +02:00
18 changed files with 542 additions and 107 deletions
+3
View File
@@ -6,12 +6,15 @@ on:
- '**'
tags-ignore:
- '*'
- '**'
- 'v*'
pull_request:
workflow_dispatch:
jobs:
test:
name: Run Component Tests & Pipeline Verification
if: "!startsWith(github.ref, 'refs/tags/')"
runs-on: ubuntu-latest
steps:
- name: Checkout Code
-44
View File
@@ -1,44 +0,0 @@
name: Release Linux Binaries
on:
release:
types: [published, created]
push:
tags:
- '*'
- '**'
workflow_dispatch:
inputs:
tag:
description: 'Release tag (optional)'
required: false
jobs:
release-linux:
name: Build & Release Linux Binaries
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Python and Build Dependencies
run: |
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y
apt-get install -y python3 python3-pip python3-venv binutils zip
fi
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
- name: Compile Standalone Linux Binaries
run: |
python3 compilation/package_dist.py --target linux
- name: Publish Linux Release Assets
env:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_REF_NAME: ${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
run: |
python3 compilation/upload_release.py --skip-build
@@ -1,4 +1,4 @@
name: Release Windows Binaries & Installers
name: Release Binaries & Installers
on:
push:
@@ -7,22 +7,62 @@ on:
workflow_dispatch:
inputs:
tag:
description: 'Release tag (optional)'
description: 'Release tag to publish assets to (default: v2.0.1)'
required: false
default: 'v2.0.1'
jobs:
release-linux:
name: Build & Release Linux Binaries
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Install Python and Build Dependencies
run: |
if command -v apt-get >/dev/null 2>&1; then
apt-get update -y
apt-get install -y python3 python3-pip python3-venv binutils zip
fi
python3 -m pip install --upgrade pip --break-system-packages || python3 -m pip install --upgrade pip || true
pip3 install pyinstaller -r compilation/requirements.txt --break-system-packages || pip3 install pyinstaller -r compilation/requirements.txt
- name: Compile Standalone Linux Binaries
run: |
python3 compilation/package_dist.py --target linux
- name: Publish Linux Release Assets
env:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
run: |
python3 compilation/upload_release.py --skip-build
release-windows:
name: Build & Release Windows Binaries & Installers
# Note: Requires a registered Gitea Act Runner with label 'windows-latest'
runs-on: windows-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
shell: powershell
run: |
$server = "${{ github.server_url }}"
$token = "${{ secrets.TAG_TOKEN || github.token }}"
$repo = "${{ github.repository }}"
$cleanUrl = $server -replace "^https?://", ""
$authUrl = "https://${token}@${cleanUrl}/${repo}.git"
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
continue-on-error: true
if (-not (Test-Path ".git")) {
git init
git remote add origin $authUrl
} else {
git remote set-url origin $authUrl
}
git fetch --depth 1 origin "${{ github.sha }}"
git checkout -f FETCH_HEAD
- name: Install Dependencies
shell: powershell
@@ -73,7 +113,7 @@ jobs:
GITEA_TOKEN: ${{ secrets.TAG_TOKEN || github.token }}
GITEA_SERVER_URL: ${{ github.server_url }}
GITEA_REPOSITORY: ${{ github.repository }}
GITEA_REF_NAME: ${{ github.event.release.tag_name || inputs.tag || github.ref_name }}
GITEA_REF_NAME: ${{ inputs.tag || github.event.release.tag_name || github.ref_name }}
run: |
$py = "python"
if (-not (Get-Command "python" -ErrorAction SilentlyContinue)) {
+38 -11
View File
@@ -114,6 +114,15 @@ graph TB
- If `active_seats >= max_seats`, the hub rejects registration with `HTTP 403 (License seat limit reached)`.
- Existing registered clients can re-enroll / renew seamlessly without consuming additional seats.
### 4. In-Flight Certificate Watchdog & Automated Self-Healing Renewal
- **Continuous Hub PKI Watchdog**:
- The server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, running every 12 hours) alongside startup checks.
- The hub automatically inspects expiration dates of both the Root CA (`ca.crt`) and the Server TLS certificate (`server.crt`).
- If either certificate is within 30 days of expiration, the server regenerates certificates (backing up previous keys as `ca.crt.<timestamp>.bak`) and dynamically reloads its active `ssl.SSLContext` in memory without dropping socket connections or restarting the service.
- **Client Proactive Check & Reactive Self-Healing**:
- **Proactive Renewal**: Edge clients inspect `client.crt` before every run cycle. If the certificate expires in less than 30 days, it automatically contacts `/api/client/enroll` to renew its certificate.
- **Reactive Self-Healing**: If the server hub Root CA rotates or a handshake fails with `ssl.SSLError` / `SSLCertVerificationError`, edge clients catch the verification exception, re-bootstrap certificate enrollment against the hub, and re-establish the connection cleanly without human intervention.
---
## Cloud-Side Temporal Persistence & 4-Run Rule
@@ -254,9 +263,8 @@ LOGAR provides production-grade installation scripts and installer builders for
LOGAR/
├── .gitea/
│ └── workflows/
│ ├── ci.yml # CI pipeline: syntax, 22 unit tests & mTLS pipeline test
── release-linux.yml # Linux release workflow (compiles binaries & checksums)
│ └── release-windows.yml # Windows release workflow (compiles .exe & Inno Setup installers)
│ ├── ci.yml # CI pipeline: syntax, 25 unit tests & mTLS pipeline test
── release.yml # Consolidated release workflow (Linux binaries & Windows installers)
├── compilation/ # Packaging, installers, and release automation
│ ├── install_linux_client.sh # Automated Linux client systemd installation script
│ ├── install_linux_server.sh # Automated Linux server systemd installation script
@@ -334,10 +342,11 @@ LOGAR/
```bash
python -m unittest discover -s tests -v
```
Runs all **22 unit tests**, covering:
Runs all **25 unit tests**, covering:
- Dynamic Root CA generation and server TLS certificate issuance.
- Dynamic client certificate issuance with CN and authority key extensions.
- Enrollment secret authentication, seat limits, and certificate revocation.
- Proactive certificate validity checks, Root CA auto-renewal, and in-flight server SSLContext reload.
- Windows & Linux event log collection, deduplication, and mTLS certificate verification.
- 12-hour evaluation window and 4-run rule progression.
@@ -356,13 +365,31 @@ Tests client enrollment, secret rejection, mTLS TLS 1.3 socket handshake, warnin
---
## Automated Releases via Gitea Actions
## Releases & Binary Distribution
Releases are triggered automatically on tag push (`v*`):
Releases can be built and published through three complementary channels:
### 1. Tag Push Automation (Gitea Actions)
Pushing a release tag automatically triggers the build workflows:
```bash
git tag v1.0.4
git push origin v1.0.4
git tag v2.0.1
git push origin v2.0.1
```
The consolidated workflow **`release.yml`** defines two parallel jobs:
- **`release-linux`** (`ubuntu-latest`): Compiles standalone native ELF binaries (`Linux_Client.bin`, `Server.bin`) and checksums.
- **`release-windows`** (`windows-latest`): Compiles Windows executables (`Win_Client.exe`, `Server.exe`), builds Inno Setup installers, and publishes checksums (requires self-hosted Windows Act Runner).
### 2. Manual Workflow Dispatch (Gitea UI)
Workflows can be manually triggered on demand from the Gitea web interface:
1. Navigate to **Actions** $\rightarrow$ **Release Binaries & Installers** (`release.yml`).
2. Click **Run workflow**, set the release tag (defaults to `v2.0.1`), and run.
### 3. Native Local Windows Build & Direct Release Upload
For environments without a registered Windows CI runner, Windows executables can be built and published directly to Gitea releases:
```powershell
# 1. Build Windows binaries locally
python compilation/package_dist.py --target windows
# 2. Upload assets and release notes directly to the Gitea release
python compilation/upload_release.py --tag v2.0.1 --token <GITEA_TOKEN> --skip-build
```
Two dedicated workflows run in parallel:
- **`release-linux.yml`** (`ubuntu-latest`): Compiles `Linux_Client.bin` and `Server.bin`, generating checksums.
- **`release-windows.yml`** (`windows-latest`): Compiles `Win_Client.exe` and `Server.exe`, builds Inno Setup installers (`LOGAR-Client-Setup.exe`, `LOGAR-Server-Setup.exe`), and uploads all artifacts.
+59 -12
View File
@@ -1,13 +1,60 @@
# LOGAR Release v1.0.2
# LOGAR Release v2.0.1
Maintenance and deployment release consolidating Gitea Actions release automation into a unified single workflow file, standardizing release dispatching across platforms, and bumping version definitions across server hub and Windows installers.
### Key Highlights & Changes in v2.0.1:
- **Consolidated Single Release Automation Workflow (`.gitea/workflows/release.yml`)**:
- Unified separate platform release files into a single, cohesive workflow (`release.yml`) running parallel jobs (`release-linux` on `ubuntu-latest` and `release-windows` on `windows-latest`).
- Standardized tag matching for Gitea Actions on `push: tags: ['v*']`.
- Added streamlined manual `workflow_dispatch` triggers with automated release tag defaulting (`v2.0.1`).
- Retired deprecated `release-linux.yml` and `release-windows.yml` files.
- **Installer & Engine Version Bump**:
- Updated FastAPI Hub engine version to `2.0.1` in `src/Server.py`.
- Bumped Inno Setup Windows Client Installer (`compilation/installer_client.iss`) `AppVersion` to `2.0.1`.
- Bumped Inno Setup Windows Server Installer (`compilation/installer_server.iss`) `AppVersion` to `2.0.1`.
- **Distribution & Release Documentation**:
- Updated root and distribution documentation across all 5 deployment targets to reflect the 25 passing unit tests and tripartite release options.
---
# LOGAR Release v2.0.0
Major architectural release introducing Mutual TLS 1.3 (mTLS) transport security, built-in dynamic PKI & license accounting, in-flight certificate validity monitoring and auto-renewal, and automated Windows and Linux service installers.
### Key Highlights & Changes in v2.0.0:
- **mTLS 1.3 Transport Security & Runtime Licensing**:
- Replaced legacy plain TCP sockets with mutual TLS 1.3 authentication (`ssl.CERT_REQUIRED`, TLS 1.3 minimum version).
- Hub dynamically validates incoming client Common Name (`client_id`) against active license seats in SQLite during the TLS handshake.
- Drops unauthorized, un-enrolled, or revoked clients at the transport layer before payload reading.
- **Dynamic Hub PKI Engine (`src/server_enrollment.py`)**:
- Automatically initializes an internal RSA-4096 Root CA (`ca.crt` / `ca.key`).
- Generates RSA-2048 Server TLS certificates with SANs for localhost, loopback, and server hostnames.
- Full OpenSSL 3.x and Python 3.123.14 compatibility via `SubjectKeyIdentifier` and `AuthorityKeyIdentifier` certificate extensions.
- Generates and signs client certificates on demand via `POST /api/client/enroll`.
- **In-Flight Certificate Validity Watchdog & Dynamic SSLContext Reloading**:
- Server hub runs a continuous background watchdog coroutine (`cert_validity_watchdog`, evaluated every 12 hours) alongside startup checks.
- Automatically checks Root CA and server TLS certificate expiration against a 30-day threshold.
- Generates renewed certificates on disk with timestamped backups (`.bak`), and reloads active `ssl.SSLContext` in memory dynamically without dropping socket listeners or restarting the background service.
- **Client Proactive Expiry Check & Reactive Self-Healing Auto-Renewal**:
- **Proactive**: Forwarders (`Win_Client.py` and `Linux_Client.py`) evaluate `client.crt` validity before each run, auto-renewing via `/api/client/enroll` if expiring within 30 days.
- **Reactive**: If the hub rotates its Root CA or a TLS verification error (`ssl.SSLError` / `SSLCertVerificationError`) occurs, clients automatically catch the error, re-enroll with the hub using their enrollment secret, and reconnect cleanly.
- **Database Schema & License Quota Accounting**:
- SQLite tables `license_config` (`max_seats`, `enrollment_secret`) and `clients` (`client_id`, `hostname`, `os_type`, `cert_fingerprint`, `status`, timestamps).
- Enforces seat limits on enrollment (`HTTP 403 License seat limit reached`) while allowing active registered nodes to re-enroll/renew indefinitely.
- Added `GET /api/clients` endpoint for license auditing and telemetry tracking.
- **Automated Service Installers**:
- **Windows**: Self-contained Inno Setup installers (`LOGAR-Client-Setup.exe` and `LOGAR-Server-Setup.exe`) bundling `nssm.exe` to register, configure, and start Windows services automatically.
- **Linux**: Automated installer scripts (`compilation/install_linux_client.sh` and `install_linux_server.sh`) deploying systemd service units with auto-restart policies.
- **CI/CD Release Workflows**:
- Windows workflow (`.gitea/workflows/release-windows.yml`) and Linux workflow (`.gitea/workflows/release-linux.yml`) automated to build native executables, installers, and upload release assets on tag push.
### Changes in this Release:
- **Warning Persistence & Immediate Error Routing**: Restructured temporal verification on the central hub so the 4-run persistence rule across the 12-hour evaluation window strictly governs `WARNING` and `INFO` events to suppress transient blips. High-severity `ERROR`, `CRITICAL`, and `FATAL` events are now promoted to `VERIFIED` immediately on their first occurrence and reported to Hermes without waiting for consecutive runs.
- **Server Deployment Packages in `out/`**: Added comprehensive deployment guides and configuration templates for both Linux Server hub (systemd service) and Windows Server hub (NSSM service / Task Scheduler) under `out/linux_server` and `out/win_server`.
- **Refactored Repository Layout**: Reorganized codebase by moving runtime forwarders and server hub into `src/`, compilation/release packaging utilities into `compilation/`, and all unit and pipeline verification tests into `tests/`.
- **Dual Platform Gitea Release Automation**: Dedicated Windows (`release-windows.yml`) and Linux (`release-linux.yml`) Gitea Actions to compile native platform binaries (`Win_Client.exe` and `Server.exe` on Windows; `Linux_Client.bin` and `Server.bin` on Linux).
- **Dedicated SHA-256 Checksums**: Release assets now include dedicated checksum files matching `[win/linux]_[client/agent]_sha256sum` (`win_client_sha256sum`, `win_agent_sha256sum`, `win_server_sha256sum`, `linux_client_sha256sum`, `linux_agent_sha256sum`, `linux_server_sha256sum`).
- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source.
- **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent.
- **24-Hour Lookback Window**: Forwarders now scan and upload events from the last 24 hours (default `--hours 24`), skipping older entries.
- **Lightweight Distribution Structure**: Cleaned `out/` to strictly contain deployment documentation and sample configurations.
- **Automated Gitea CI/CD**: Integrated push testing workflow (`ci.yml`) and multi-platform release asset packaging.
+1 -1
View File
@@ -1,6 +1,6 @@
[Setup]
AppName=LOGAR Client
AppVersion=1.0.3
AppVersion=2.0.1
DefaultDirName={autopf}\LOGAR
OutputDir=..\dist
OutputBaseFilename=LOGAR-Client-Setup
+1 -1
View File
@@ -1,6 +1,6 @@
[Setup]
AppName=LOGAR Server
AppVersion=1.0.3
AppVersion=2.0.1
DefaultDirName={autopf}\LOGAR-Server
OutputDir=..\dist
OutputBaseFilename=LOGAR-Server-Setup
+1
View File
@@ -11,6 +11,7 @@ Standalone compiled binary and automated systemd service distribution for Linux
- **Pre-compiled & Dependency-Free**: Ships as a standalone native binary (`Linux_Client.bin`). No Python environment, pip packages, or GnuPG binaries are required on the host.
- **Mutual TLS 1.3 (mTLS) Ingestion**: Streams directly over hardware-authenticated TLS 1.3 sockets with machine-bound client certificates.
- **Automated Client Enrollment**: On first run with an `enrollment_secret`, the client automatically calls `POST /api/client/enroll` on the hub, saves its certificates into `/etc/logar/certs/`, and establishes secure mTLS streaming.
- **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR` (`journalctl -p warning`). Drops debug noise and skips events older than 24 hours.
- **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking systemd journalctl cursors and microsecond timestamps) so every log record is forwarded exactly once without duplicates.
- **Fail-Safe State Commit**: State is committed only when the server returns a verified `success` response. In the event of a network outage, state remains unchanged and unsent events are retried automatically on the next run.
+1
View File
@@ -12,6 +12,7 @@ Standalone compiled binary and automated systemd service distribution for Linux
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Linux ELF binary (`Server.bin`). No Python runtime, pip dependencies, or GnuPG binaries are required on the host system.
- **Mutual TLS 1.3 (mTLS) Ingestion (Port 9443)**: Enforces mutual TLS 1.3 authentication for all incoming edge connections. Validates client certificates against an internal Root CA and verifies active licensing in SQLite.
- **Dynamic PKI & License Accounting**: Built-in Root CA generates server TLS certificates with SANs and dynamically signs client certificates via `POST /api/client/enroll` while enforcing seat limits (`max_seats`).
- **In-Flight Certificate Watchdog & Dynamic Reloading**: Continuously monitors Root CA (`ca.crt`) and Server TLS certificate (`server.crt`) validity in the background (every 12 hours). When nearing expiration (< 30 days), certificates are automatically regenerated with timestamped backups, and active `ssl.SSLContext` structures are reloaded dynamically without dropping socket connections or restarting the systemd service.
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window.
- **Embedded Hermes Reporting & Management API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
+1
View File
@@ -11,6 +11,7 @@ Standalone compiled executable and installer distribution for Windows Server and
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Windows executable (`Win_Client.exe`) or full installer (`LOGAR-Client-Setup.exe`). No Python installation, pip packages, or GnuPG binaries are required on the host.
- **Mutual TLS 1.3 (mTLS) Ingestion**: Streams directly over hardware-authenticated TLS 1.3 sockets with hardware/machine-bound client certificates.
- **Automated Client Enrollment**: On first run with an `enrollment_secret`, the client automatically calls `POST /api/client/enroll` on the hub, saves its certificates into `certs/`, and establishes secure mTLS streaming.
- **Proactive Expiry Check & Reactive Self-Healing**: Before each run, the client evaluates `client.crt` validity. If within 30 days of expiry, it automatically contacts the hub to renew certificates. If the server Root CA rotates or a TLS handshake error occurs, the client catch-heals by re-enrolling immediately and re-establishing connection without human intervention.
- **Source-Level Filtering**: Retains events spanning `INFO`, `WARNING`, and `ERROR`. Strips audit events and debug noise, skipping events older than 24 hours.
- **State Tracking & Deduplication**: Maintains persistent client state in `client_state.json` (tracking event record numbers and timestamp signatures) so every log record is forwarded exactly once without duplicates.
- **Fail-Safe State Commit**: State is committed only when the server returns a verified `success` response. In the event of a network outage, state remains unchanged and unsent events are retried automatically on the next run.
+1
View File
@@ -12,6 +12,7 @@ Standalone compiled executable and installer distribution for Windows Server env
- **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`) or full installer (`LOGAR-Server-Setup.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server.
- **Mutual TLS 1.3 (mTLS) Ingestion (Port 9443)**: Enforces mutual TLS 1.3 authentication for all incoming edge connections. Validates client certificates against an internal Root CA and verifies active licensing in SQLite.
- **Dynamic PKI & License Accounting**: Built-in Root CA generates server TLS certificates with SANs and dynamically signs client certificates via `POST /api/client/enroll` while enforcing seat limits (`max_seats`).
- **In-Flight Certificate Watchdog & Dynamic Reloading**: Continuously monitors Root CA (`ca.crt`) and Server TLS certificate (`server.crt`) validity in the background (every 12 hours). When nearing expiration (< 30 days), certificates are automatically regenerated with timestamped backups, and active `ssl.SSLContext` structures are reloaded dynamically without dropping socket connections or restarting the Windows service.
- **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events require persistence across at least 4 distinct transmission cycles within a rolling 12-hour evaluation window.
- **Embedded Hermes Reporting & Management API (Port 8443)**: Integrated REST API exposing `/api/hermes/report`, `/api/clients`, and `/api/client/enroll`.
- **State Database**: Stores issue lifecycle records, client telemetry, and licensing quotas in a local SQLite database (`logar_state.db`).
+51 -6
View File
@@ -20,17 +20,46 @@ CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def enroll_client_if_needed(hub_url: str, enrollment_secret: str, cert_dir: str, client_id: str, hostname: str, os_type: str = "linux"):
"""Bootstraps client enrollment if certificates are missing."""
def is_cert_expiring_soon(cert_path: str, threshold_days: int = 30) -> bool:
"""Checks if client certificate at cert_path is expiring within threshold_days."""
if not os.path.exists(cert_path):
return True
try:
from cryptography import x509
with open(cert_path, "r", encoding="utf-8") as f:
cert = x509.load_pem_x509_certificate(f.read().encode("utf-8"))
expiry = getattr(cert, "not_valid_after_utc", None)
if expiry is None:
expiry = cert.not_valid_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return expiry <= (now + timedelta(days=threshold_days))
except Exception:
return True
def enroll_client_if_needed(
hub_url: str,
enrollment_secret: str,
cert_dir: str,
client_id: str,
hostname: str,
os_type: str = "linux",
force_renew: bool = False,
threshold_days: int = 30
):
"""Bootstraps client enrollment if certificates are missing or expiring soon."""
os.makedirs(cert_dir, exist_ok=True)
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
if os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path):
if not force_renew and os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path):
if not is_cert_expiring_soon(cert_path, threshold_days=threshold_days):
return True
print(f"[*] Client certificate at {cert_path} is expiring within {threshold_days} days. Auto-renewing...")
print(f"[*] Bootstrapping client enrollment with LOGAR Hub at {hub_url}...")
action_name = "re-enrolling" if os.path.exists(cert_path) else "enrolling"
print(f"[*] Bootstrapping client {action_name} with LOGAR Hub at {hub_url}...")
enroll_endpoint = f"{hub_url.rstrip('/')}/api/client/enroll"
payload = {
"client_id": client_id,
@@ -60,7 +89,7 @@ def enroll_client_if_needed(hub_url: str, enrollment_secret: str, cert_dir: str,
except Exception:
pass
print(f"[+] Client enrolled successfully! Certificates saved to {os.path.abspath(cert_dir)}")
print(f"[+] Client certificates updated successfully in {os.path.abspath(cert_dir)}")
return True
@@ -302,6 +331,7 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
machine_id = get_machine_identifier()
# Attempt automatic enrollment bootstrap if certs are missing and secret is provided
hub_url = None
if enrollment_secret:
hermes_host = config.get("hermes_host", server_host)
hermes_port = config.get("hermes_port", 8443)
@@ -318,7 +348,22 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
if has_mtls_certs:
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over mTLS (TLS 1.3)...")
with get_tls_socket(server_host, server_port, cert_dir) as sock:
sock = None
try:
sock = get_tls_socket(server_host, server_port, cert_dir)
except (ssl.SSLError, ssl.CertificateError, ConnectionResetError) as tls_err:
if enrollment_secret and hub_url:
print(f"[!] TLS handshake error ({tls_err}). Re-enrolling with LOGAR Hub...")
try:
enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, machine_id, machine_id, os_type="linux", force_renew=True)
sock = get_tls_socket(server_host, server_port, cert_dir)
except Exception as retry_err:
print(f"[!] Re-enrollment or reconnection retry failed: {retry_err}")
raise
else:
raise
with sock:
payload = {
"server": machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
+78 -2
View File
@@ -38,7 +38,7 @@ DEFAULT_DB_FILE = "logar_state.db"
EVALUATION_WINDOW_HOURS = 12
RUN_THRESHOLD = 4
app = FastAPI(title="LOGAR Cloud Ingestion & Hermes Hub", version="2.0.0")
app = FastAPI(title="LOGAR Cloud Ingestion & Hermes Hub", version="2.0.1")
# Global context holding server state
SERVER_STATE: Dict[str, Any] = {}
@@ -322,6 +322,72 @@ def init_mtls_server_context(cert_dir: str = "certs") -> ssl.SSLContext:
return ctx
def reload_mtls_context(ssl_ctx: ssl.SSLContext, cert_dir: str = "certs"):
"""
Dynamically reloads server certificate chain and Root CA in an active SSLContext.
Allows in-flight TLS certificate rotation without dropping the listening socket.
"""
ca_file = os.path.join(cert_dir, "ca.crt")
srv_cert = os.path.join(cert_dir, "server.crt")
srv_key = os.path.join(cert_dir, "server.key")
ssl_ctx.load_cert_chain(certfile=srv_cert, keyfile=srv_key)
ssl_ctx.load_verify_locations(cafile=ca_file)
def check_and_rotate_server_certs(
cert_dir: str = "certs",
hostnames: Optional[List[str]] = None,
threshold_days: int = 30
) -> bool:
"""
Checks if Root CA or server TLS certificate are expiring within threshold_days.
If so, regenerates them, dynamically reloads the active SSLContext in-place,
and updates the server's in-memory CA reference so future enrollments use the new CA.
Returns True if renewed/reloaded, False otherwise.
"""
ca_renewed, srv_renewed = enrollment.check_and_renew_hub_pki(
cert_dir=cert_dir,
hostnames=hostnames,
threshold_days=threshold_days
)
if ca_renewed or srv_renewed:
print(f"[!] Server Hub PKI certificates renewed (CA renewed: {ca_renewed}, Server cert renewed: {srv_renewed}).")
ca_cert, ca_key, ca_pem, ca_key_pem = enrollment.generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
SERVER_STATE["ca_cert"] = ca_cert
SERVER_STATE["ca_key"] = ca_key
SERVER_STATE["ca_cert_pem"] = ca_pem
ssl_ctx = SERVER_STATE.get("ssl_ctx")
if ssl_ctx is not None:
try:
reload_mtls_context(ssl_ctx, cert_dir=cert_dir)
print("[+] In-flight mTLS SSLContext successfully reloaded with updated certificates.")
except Exception as e:
print(f"[!] Failed to reload in-flight SSLContext: {e}")
return True
return False
async def cert_validity_watchdog(interval_seconds: int = 43200, threshold_days: int = 30):
"""
Periodically checks the validity of Hub Root CA and Server TLS certificates (default every 12 hours).
Triggers in-flight renewal and dynamic context reloading if expiration is within threshold_days.
"""
config = SERVER_STATE.get("config", {})
cert_dir = config.get("cert_dir", "certs")
hostnames = [config.get("tcp_host", "0.0.0.0"), "127.0.0.1", "localhost"]
while True:
try:
await asyncio.sleep(interval_seconds)
check_and_rotate_server_certs(cert_dir=cert_dir, hostnames=hostnames, threshold_days=threshold_days)
except asyncio.CancelledError:
break
except Exception as e:
print(f"[!] Exception in cert_validity_watchdog: {e}")
async def handle_socket_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
"""
mTLS TCP socket handler.
@@ -631,10 +697,20 @@ async def run_server():
print(f"[*] Hermes Reporting API available at http://{hermes_host}:{hermes_port}/api/hermes/report")
print(f"[*] Client Enrollment API available at http://{hermes_host}:{hermes_port}/api/client/enroll")
watchdog_task = asyncio.create_task(cert_validity_watchdog())
try:
await asyncio.gather(
tcp_server.serve_forever(),
uv_server.serve()
uv_server.serve(),
watchdog_task
)
finally:
watchdog_task.cancel()
try:
await watchdog_task
except asyncio.CancelledError:
pass
def main():
+51 -6
View File
@@ -24,17 +24,46 @@ CONFIG_FILE_NAME = "client_config.json"
STATE_FILE_NAME = "client_state.json"
def enroll_client_if_needed(hub_url: str, enrollment_secret: str, cert_dir: str, client_id: str, hostname: str, os_type: str = "windows"):
"""Bootstraps client enrollment if certificates are missing."""
def is_cert_expiring_soon(cert_path: str, threshold_days: int = 30) -> bool:
"""Checks if client certificate at cert_path is expiring within threshold_days."""
if not os.path.exists(cert_path):
return True
try:
from cryptography import x509
with open(cert_path, "r", encoding="utf-8") as f:
cert = x509.load_pem_x509_certificate(f.read().encode("utf-8"))
expiry = getattr(cert, "not_valid_after_utc", None)
if expiry is None:
expiry = cert.not_valid_after.replace(tzinfo=timezone.utc)
now = datetime.now(timezone.utc)
return expiry <= (now + timedelta(days=threshold_days))
except Exception:
return True
def enroll_client_if_needed(
hub_url: str,
enrollment_secret: str,
cert_dir: str,
client_id: str,
hostname: str,
os_type: str = "windows",
force_renew: bool = False,
threshold_days: int = 30
):
"""Bootstraps client enrollment if certificates are missing or expiring soon."""
os.makedirs(cert_dir, exist_ok=True)
ca_path = os.path.join(cert_dir, "ca.crt")
cert_path = os.path.join(cert_dir, "client.crt")
key_path = os.path.join(cert_dir, "client.key")
if os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path):
if not force_renew and os.path.exists(ca_path) and os.path.exists(cert_path) and os.path.exists(key_path):
if not is_cert_expiring_soon(cert_path, threshold_days=threshold_days):
return True
print(f"[*] Client certificate at {cert_path} is expiring within {threshold_days} days. Auto-renewing...")
print(f"[*] Bootstrapping client enrollment with LOGAR Hub at {hub_url}...")
action_name = "re-enrolling" if os.path.exists(cert_path) else "enrolling"
print(f"[*] Bootstrapping client {action_name} with LOGAR Hub at {hub_url}...")
enroll_endpoint = f"{hub_url.rstrip('/')}/api/client/enroll"
payload = {
"client_id": client_id,
@@ -64,7 +93,7 @@ def enroll_client_if_needed(hub_url: str, enrollment_secret: str, cert_dir: str,
except Exception:
pass
print(f"[+] Client enrolled successfully! Certificates saved to {os.path.abspath(cert_dir)}")
print(f"[+] Client certificates updated successfully in {os.path.abspath(cert_dir)}")
return True
@@ -269,6 +298,7 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
machine_id = get_machine_identifier()
# Attempt automatic enrollment bootstrap if certs are missing and secret is provided
hub_url = None
if enrollment_secret:
hermes_host = config.get("hermes_host", server_host)
hermes_port = config.get("hermes_port", 8443)
@@ -285,7 +315,22 @@ def send_encrypted_logs_over_socket(config: dict, logs: list):
if has_mtls_certs:
print(f"[*] Connecting to LOGAR server at {server_host}:{server_port} over mTLS (TLS 1.3)...")
with get_tls_socket(server_host, server_port, cert_dir) as sock:
sock = None
try:
sock = get_tls_socket(server_host, server_port, cert_dir)
except (ssl.SSLError, ssl.CertificateError, ConnectionResetError) as tls_err:
if enrollment_secret and hub_url:
print(f"[!] TLS handshake error ({tls_err}). Re-enrolling with LOGAR Hub...")
try:
enroll_client_if_needed(hub_url, enrollment_secret, cert_dir, machine_id, machine_id, os_type="windows", force_renew=True)
sock = get_tls_socket(server_host, server_port, cert_dir)
except Exception as retry_err:
print(f"[!] Re-enrollment or reconnection retry failed: {retry_err}")
raise
else:
raise
with sock:
payload = {
"server": machine_id,
"timestamp": datetime.now(timezone.utc).isoformat(),
+117 -4
View File
@@ -14,23 +14,59 @@ def calculate_cert_fingerprint(cert_pem: str) -> str:
return cert.fingerprint(hashes.SHA256()).hex().upper()
def generate_ca_if_needed(cert_dir: str = "certs", common_name: str = "LOGAR-Root-CA") -> Tuple[x509.Certificate, rsa.RSAPrivateKey, str, str]:
def is_cert_expiring_soon(cert_pem: str, threshold_days: int = 30) -> bool:
"""
Checks if a PEM-encoded X.509 certificate expires within `threshold_days` (or is already expired).
Returns True if expiring soon or expired, False otherwise.
"""
try:
cert = x509.load_pem_x509_certificate(cert_pem.encode("utf-8"))
expiry = getattr(cert, "not_valid_after_utc", None)
if expiry is None:
expiry = cert.not_valid_after.replace(tzinfo=datetime.timezone.utc)
now = datetime.datetime.now(datetime.timezone.utc)
return expiry <= (now + datetime.timedelta(days=threshold_days))
except Exception:
return True
def generate_ca_if_needed(
cert_dir: str = "certs",
common_name: str = "LOGAR-Root-CA",
force_renew: bool = False,
threshold_days: int = 30
) -> Tuple[x509.Certificate, rsa.RSAPrivateKey, str, str]:
"""
Loads an existing Root CA or generates a self-signed Root CA certificate and private key.
If existing CA cert is expiring within threshold_days (or force_renew is True), regenerates it.
Returns (ca_cert_obj, ca_key_obj, ca_cert_pem, ca_key_pem).
"""
os.makedirs(cert_dir, exist_ok=True)
ca_cert_path = os.path.join(cert_dir, "ca.crt")
ca_key_path = os.path.join(cert_dir, "ca.key")
if os.path.exists(ca_cert_path) and os.path.exists(ca_key_path):
if not force_renew and os.path.exists(ca_cert_path) and os.path.exists(ca_key_path):
with open(ca_cert_path, "r", encoding="utf-8") as f:
ca_cert_pem = f.read()
with open(ca_key_path, "r", encoding="utf-8") as f:
ca_key_pem = f.read()
try:
ca_cert = x509.load_pem_x509_certificate(ca_cert_pem.encode("utf-8"))
ca_key = serialization.load_pem_private_key(ca_key_pem.encode("utf-8"), password=None)
if not is_cert_expiring_soon(ca_cert_pem, threshold_days=threshold_days):
return ca_cert, ca_key, ca_cert_pem, ca_key_pem
except Exception:
pass
# Create timestamped backup of previous CA if present
if os.path.exists(ca_cert_path):
try:
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
os.replace(ca_cert_path, f"{ca_cert_path}.{timestamp}.bak")
if os.path.exists(ca_key_path):
os.replace(ca_key_path, f"{ca_key_path}.{timestamp}.bak")
except Exception:
pass
# Generate RSA 4096 private key for Root CA
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=4096)
@@ -96,24 +132,41 @@ def generate_server_cert_if_needed(
ca_key: rsa.RSAPrivateKey,
hostnames: Optional[List[str]] = None,
cert_dir: str = "certs",
days_valid: int = 825
days_valid: int = 825,
force_renew: bool = False,
threshold_days: int = 30
) -> Tuple[x509.Certificate, rsa.RSAPrivateKey, str, str]:
"""
Loads an existing server certificate or generates a new server TLS certificate signed by the Root CA.
If existing server cert is expiring within threshold_days (or force_renew is True), regenerates it.
Includes SANs for localhost, 127.0.0.1, and specified hostnames.
"""
os.makedirs(cert_dir, exist_ok=True)
server_cert_path = os.path.join(cert_dir, "server.crt")
server_key_path = os.path.join(cert_dir, "server.key")
if os.path.exists(server_cert_path) and os.path.exists(server_key_path):
if not force_renew and os.path.exists(server_cert_path) and os.path.exists(server_key_path):
with open(server_cert_path, "r", encoding="utf-8") as f:
server_cert_pem = f.read()
with open(server_key_path, "r", encoding="utf-8") as f:
server_key_pem = f.read()
try:
srv_cert = x509.load_pem_x509_certificate(server_cert_pem.encode("utf-8"))
srv_key = serialization.load_pem_private_key(server_key_pem.encode("utf-8"), password=None)
if not is_cert_expiring_soon(server_cert_pem, threshold_days=threshold_days):
return srv_cert, srv_key, server_cert_pem, server_key_pem
except Exception:
pass
# Create timestamped backup of previous server cert if present
if os.path.exists(server_cert_path):
try:
timestamp = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d_%H%M%S")
os.replace(server_cert_path, f"{server_cert_path}.{timestamp}.bak")
if os.path.exists(server_key_path):
os.replace(server_key_path, f"{server_key_path}.{timestamp}.bak")
except Exception:
pass
server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
subject = x509.Name([
@@ -265,3 +318,63 @@ def issue_client_cert(
).decode("utf-8")
return cert_pem, key_pem
def check_and_renew_hub_pki(
cert_dir: str = "certs",
hostnames: Optional[List[str]] = None,
threshold_days: int = 30
) -> Tuple[bool, bool]:
"""
Evaluates expiration status of Root CA and Server TLS certificates.
If CA certificate is expiring within threshold_days (or missing):
- Regenerates Root CA.
- Automatically regenerates Server TLS certificate (since CA issuer changed).
- Returns (ca_renewed=True, server_renewed=True)
Else if Server TLS certificate is expiring within threshold_days (or missing):
- Regenerates Server TLS certificate signed by existing Root CA.
- Returns (ca_renewed=False, server_renewed=True)
Otherwise:
- Returns (False, False)
"""
os.makedirs(cert_dir, exist_ok=True)
ca_cert_path = os.path.join(cert_dir, "ca.crt")
server_cert_path = os.path.join(cert_dir, "server.crt")
renew_ca = False
renew_server = False
if not os.path.exists(ca_cert_path):
renew_ca = True
else:
try:
with open(ca_cert_path, "r", encoding="utf-8") as f:
ca_pem = f.read()
if is_cert_expiring_soon(ca_pem, threshold_days=threshold_days):
renew_ca = True
except Exception:
renew_ca = True
if renew_ca:
ca_cert, ca_key, _, _ = generate_ca_if_needed(cert_dir=cert_dir, force_renew=True)
generate_server_cert_if_needed(ca_cert, ca_key, hostnames=hostnames, cert_dir=cert_dir, force_renew=True)
return True, True
if not os.path.exists(server_cert_path):
renew_server = True
else:
try:
with open(server_cert_path, "r", encoding="utf-8") as f:
srv_pem = f.read()
if is_cert_expiring_soon(srv_pem, threshold_days=threshold_days):
renew_server = True
except Exception:
renew_server = True
if renew_server:
ca_cert, ca_key, _, _ = generate_ca_if_needed(cert_dir=cert_dir, force_renew=False)
generate_server_cert_if_needed(ca_cert, ca_key, hostnames=hostnames, cert_dir=cert_dir, force_renew=True)
return False, True
return False, False
+21
View File
@@ -223,6 +223,27 @@ class TestLinuxClientComponent(unittest.TestCase):
finally:
shutil.rmtree(test_dir, ignore_errors=True)
def test_client_certificate_validity_and_proactive_check(self):
import shutil
test_dir = "test_linux_client_validity"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, _, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("linux-validity-test", ca_cert, ca_key, days_valid=365)
cert_path = os.path.join(test_dir, "client.crt")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(client_cert_pem)
# Newly issued cert (365 days) is not expiring soon at 30 days
self.assertFalse(Linux_Client.is_cert_expiring_soon(cert_path, threshold_days=30))
# Large threshold (500 days) reports expiring soon
self.assertTrue(Linux_Client.is_cert_expiring_soon(cert_path, threshold_days=500))
# Non-existent file reports expiring / missing
self.assertTrue(Linux_Client.is_cert_expiring_soon(os.path.join(test_dir, "missing.crt")))
finally:
shutil.rmtree(test_dir, ignore_errors=True)
if __name__ == "__main__":
unittest.main()
+37
View File
@@ -291,6 +291,43 @@ class TestServerComponent(unittest.TestCase):
if os.path.exists(test_cert_dir):
shutil.rmtree(test_cert_dir, ignore_errors=True)
def test_cert_validity_and_hub_pki_renewal(self):
test_cert_dir = "test_certs_renew"
try:
ca_cert, ca_key, ca_pem, _ = Server.enrollment.generate_ca_if_needed(cert_dir=test_cert_dir)
srv_cert, srv_key, srv_pem, _ = Server.enrollment.generate_server_cert_if_needed(
ca_cert, ca_key, hostnames=["127.0.0.1"], cert_dir=test_cert_dir
)
# 1. Freshly generated certificates should NOT be expiring soon with standard 30-day threshold
self.assertFalse(Server.enrollment.is_cert_expiring_soon(ca_pem, threshold_days=30))
self.assertFalse(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=30))
# 2. Huge threshold (e.g. 5000 days) should flag expiration
self.assertTrue(Server.enrollment.is_cert_expiring_soon(srv_pem, threshold_days=5000))
# 3. check_and_renew_hub_pki with standard threshold should report no renewal needed
ca_renewed, srv_renewed = Server.enrollment.check_and_renew_hub_pki(cert_dir=test_cert_dir, threshold_days=30)
self.assertFalse(ca_renewed)
self.assertFalse(srv_renewed)
# 4. In-flight reload of SSLContext
ssl_ctx = Server.init_mtls_server_context(cert_dir=test_cert_dir)
Server.SERVER_STATE["ssl_ctx"] = ssl_ctx
Server.SERVER_STATE["config"] = {"db_path": self.test_db, "cert_dir": test_cert_dir, "tcp_host": "127.0.0.1"}
# Trigger rotation using high threshold
rotated = Server.check_and_rotate_server_certs(cert_dir=test_cert_dir, hostnames=["127.0.0.1"], threshold_days=5000)
self.assertTrue(rotated)
# Check that backup files were generated
bak_files = [f for f in os.listdir(test_cert_dir) if f.endswith(".bak")]
self.assertGreater(len(bak_files), 0)
finally:
import shutil
if os.path.exists(test_cert_dir):
shutil.rmtree(test_cert_dir, ignore_errors=True)
if __name__ == "__main__":
unittest.main()
+21
View File
@@ -209,6 +209,27 @@ class TestWinClientComponent(unittest.TestCase):
finally:
shutil.rmtree(test_dir, ignore_errors=True)
def test_client_certificate_validity_and_proactive_check(self):
import shutil
test_dir = "test_win_client_validity"
os.makedirs(test_dir, exist_ok=True)
try:
from src import server_enrollment as se
ca_cert, ca_key, _, _ = se.generate_ca_if_needed(test_dir)
client_cert_pem, client_key_pem = se.issue_client_cert("win-validity-test", ca_cert, ca_key, days_valid=365)
cert_path = os.path.join(test_dir, "client.crt")
with open(cert_path, "w", encoding="utf-8") as f:
f.write(client_cert_pem)
# Newly issued cert (365 days) is not expiring soon at 30 days
self.assertFalse(Win_Client.is_cert_expiring_soon(cert_path, threshold_days=30))
# Large threshold (500 days) reports expiring soon
self.assertTrue(Win_Client.is_cert_expiring_soon(cert_path, threshold_days=500))
# Non-existent file reports expiring / missing
self.assertTrue(Win_Client.is_cert_expiring_soon(os.path.join(test_dir, "missing.crt")))
finally:
shutil.rmtree(test_dir, ignore_errors=True)
if __name__ == "__main__":
unittest.main()