Fix agent keystore initialization order

This commit is contained in:
2025-10-17 20:44:26 -06:00
parent 98ee77caca
commit 418e99c8c0
8 changed files with 205 additions and 28 deletions

View File

@@ -125,6 +125,24 @@ def _agent_guid_path() -> str:
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID')) return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID'))
def _settings_dir():
try:
return os.path.join(_find_project_root(), 'Agent', 'Borealis', 'Settings')
except Exception:
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings'))
_KEY_STORE_INSTANCE = None
def _key_store() -> AgentKeyStore:
global _KEY_STORE_INSTANCE
if _KEY_STORE_INSTANCE is None:
scope = 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER'
_KEY_STORE_INSTANCE = AgentKeyStore(_settings_dir(), scope=scope)
return _KEY_STORE_INSTANCE
def _persist_agent_guid_local(guid: str): def _persist_agent_guid_local(guid: str):
guid = _normalize_agent_guid(guid) guid = _normalize_agent_guid(guid)
if not guid: if not guid:
@@ -1029,23 +1047,6 @@ def _collect_heartbeat_metrics() -> Dict[str, Any]:
def _settings_dir(): def _settings_dir():
try:
return os.path.join(_find_project_root(), 'Agent', 'Borealis', 'Settings')
except Exception:
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings'))
_KEY_STORE_INSTANCE = None
def _key_store() -> AgentKeyStore:
global _KEY_STORE_INSTANCE
if _KEY_STORE_INSTANCE is None:
scope = 'SYSTEM' if SYSTEM_SERVICE_MODE else 'CURRENTUSER'
_KEY_STORE_INSTANCE = AgentKeyStore(_settings_dir(), scope=scope)
return _KEY_STORE_INSTANCE
SERVER_CERT_PATH = _key_store().server_certificate_path() SERVER_CERT_PATH = _key_store().server_certificate_path()

View File

@@ -39,7 +39,9 @@ def _restrict_permissions(path: str) -> None:
def _protect(data: bytes, *, scope_system: bool) -> bytes: def _protect(data: bytes, *, scope_system: bool) -> bytes:
if not IS_WINDOWS or not win32crypt: if not IS_WINDOWS or not win32crypt:
return data return data
flags = win32crypt.CRYPTPROTECT_LOCAL_MACHINE if scope_system else 0 flags = 0
if scope_system:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined] protected = win32crypt.CryptProtectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
return protected[1] return protected[1]
@@ -47,7 +49,9 @@ def _protect(data: bytes, *, scope_system: bool) -> bytes:
def _unprotect(data: bytes, *, scope_system: bool) -> bytes: def _unprotect(data: bytes, *, scope_system: bool) -> bytes:
if not IS_WINDOWS or not win32crypt: if not IS_WINDOWS or not win32crypt:
return data return data
flags = win32crypt.CRYPTPROTECT_LOCAL_MACHINE if scope_system else 0 flags = 0
if scope_system:
flags = getattr(win32crypt, "CRYPTPROTECT_LOCAL_MACHINE", 0x4)
unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined] unwrapped = win32crypt.CryptUnprotectData(data, None, None, None, None, flags) # type: ignore[attr-defined]
return unwrapped[1] return unwrapped[1]

View File

@@ -7,15 +7,17 @@ from __future__ import annotations
import hashlib import hashlib
import time import time
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
import jwt import jwt
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import ed25519
_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" from Modules.runtime import ensure_runtime_dir, runtime_path
_KEY_DIR = runtime_path("auth_keys")
_KEY_FILE = _KEY_DIR / "borealis-jwt-ed25519.key" _KEY_FILE = _KEY_DIR / "borealis-jwt-ed25519.key"
_LEGACY_KEY_FILE = runtime_path("keys") / "borealis-jwt-ed25519.key"
class JWTService: class JWTService:
@@ -96,11 +98,17 @@ def load_service() -> JWTService:
def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey: def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey:
_KEY_DIR.mkdir(parents=True, exist_ok=True) ensure_runtime_dir("auth_keys")
_migrate_legacy_key_if_present()
if _KEY_FILE.exists(): if _KEY_FILE.exists():
with _KEY_FILE.open("rb") as fh: with _KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None) return serialization.load_pem_private_key(fh.read(), password=None)
if _LEGACY_KEY_FILE.exists():
with _LEGACY_KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None)
private_key = ed25519.Ed25519PrivateKey.generate() private_key = ed25519.Ed25519PrivateKey.generate()
pem = private_key.private_bytes( pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
@@ -116,3 +124,17 @@ def _load_or_create_private_key() -> ed25519.Ed25519PrivateKey:
pass pass
return private_key return private_key
def _migrate_legacy_key_if_present() -> None:
if not _LEGACY_KEY_FILE.exists() or _KEY_FILE.exists():
return
try:
ensure_runtime_dir("auth_keys")
try:
_LEGACY_KEY_FILE.replace(_KEY_FILE)
except Exception:
_KEY_FILE.write_bytes(_LEGACY_KEY_FILE.read_bytes())
except Exception:
return

View File

@@ -19,7 +19,9 @@ from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import NameOID from cryptography.x509.oid import NameOID
_CERT_DIR = Path(__file__).resolve().parent.parent / "certs" from Modules.runtime import ensure_runtime_dir, runtime_path
_CERT_DIR = runtime_path("certs")
_CERT_FILE = _CERT_DIR / "borealis-server-cert.pem" _CERT_FILE = _CERT_DIR / "borealis-server-cert.pem"
_KEY_FILE = _CERT_DIR / "borealis-server-key.pem" _KEY_FILE = _CERT_DIR / "borealis-server-key.pem"
_BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem" _BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem"
@@ -35,7 +37,7 @@ def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path
Returns (cert_path, key_path, bundle_path). Returns (cert_path, key_path, bundle_path).
""" """
_CERT_DIR.mkdir(parents=True, exist_ok=True) ensure_runtime_dir("certs")
regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists()) regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists())
if not regenerate: if not regenerate:

View File

@@ -10,11 +10,15 @@ from typing import Tuple
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives.asymmetric import ed25519
from Modules.runtime import ensure_runtime_dir, runtime_path
from .keys import base64_from_spki_der from .keys import base64_from_spki_der
_KEY_DIR = Path(__file__).resolve().parent.parent / "keys" _KEY_DIR = runtime_path("script_signing_keys")
_SIGNING_KEY_FILE = _KEY_DIR / "borealis-script-ed25519.key" _SIGNING_KEY_FILE = _KEY_DIR / "borealis-script-ed25519.key"
_SIGNING_PUB_FILE = _KEY_DIR / "borealis-script-ed25519.pub" _SIGNING_PUB_FILE = _KEY_DIR / "borealis-script-ed25519.pub"
_LEGACY_KEY_FILE = runtime_path("keys") / "borealis-script-ed25519.key"
_LEGACY_PUB_FILE = runtime_path("keys") / "borealis-script-ed25519.pub"
class ScriptSigner: class ScriptSigner:
@@ -41,11 +45,17 @@ def load_signer() -> ScriptSigner:
def _load_or_create() -> ed25519.Ed25519PrivateKey: def _load_or_create() -> ed25519.Ed25519PrivateKey:
_KEY_DIR.mkdir(parents=True, exist_ok=True) ensure_runtime_dir("script_signing_keys")
_migrate_legacy_material_if_present()
if _SIGNING_KEY_FILE.exists(): if _SIGNING_KEY_FILE.exists():
with _SIGNING_KEY_FILE.open("rb") as fh: with _SIGNING_KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None) return serialization.load_pem_private_key(fh.read(), password=None)
if _LEGACY_KEY_FILE.exists():
with _LEGACY_KEY_FILE.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None)
private_key = ed25519.Ed25519PrivateKey.generate() private_key = ed25519.Ed25519PrivateKey.generate()
pem = private_key.private_bytes( pem = private_key.private_bytes(
encoding=serialization.Encoding.PEM, encoding=serialization.Encoding.PEM,
@@ -68,3 +78,23 @@ def _load_or_create() -> ed25519.Ed25519PrivateKey:
return private_key return private_key
def _migrate_legacy_material_if_present() -> None:
if not _LEGACY_KEY_FILE.exists() or _SIGNING_KEY_FILE.exists():
return
try:
ensure_runtime_dir("script_signing_keys")
try:
_LEGACY_KEY_FILE.replace(_SIGNING_KEY_FILE)
except Exception:
_SIGNING_KEY_FILE.write_bytes(_LEGACY_KEY_FILE.read_bytes())
if _LEGACY_PUB_FILE.exists() and not _SIGNING_PUB_FILE.exists():
try:
_LEGACY_PUB_FILE.replace(_SIGNING_PUB_FILE)
except Exception:
_SIGNING_PUB_FILE.write_bytes(_LEGACY_PUB_FILE.read_bytes())
except Exception:
return

View File

@@ -0,0 +1,78 @@
"""Utility helpers for locating runtime storage paths.
The Borealis repository keeps the authoritative source code under ``Data/``
so that the bootstrap scripts can copy those assets into sibling ``Server/``
and ``Agent/`` directories for execution. Runtime artefacts such as TLS
certificates or signing keys must therefore live outside ``Data`` to avoid
polluting the template tree. This module centralises the path selection so
other modules can rely on a consistent location regardless of whether they
are executed from the copied runtime directory or directly from ``Data``
during development.
"""
from __future__ import annotations
import os
from functools import lru_cache
from pathlib import Path
from typing import Optional
def _env_path(name: str) -> Optional[Path]:
"""Return a resolved ``Path`` for the given environment variable."""
value = os.environ.get(name)
if not value:
return None
try:
return Path(value).expanduser().resolve()
except Exception:
return None
@lru_cache(maxsize=None)
def project_root() -> Path:
"""Best-effort detection of the repository root."""
env = _env_path("BOREALIS_PROJECT_ROOT")
if env:
return env
current = Path(__file__).resolve()
for parent in current.parents:
if (parent / "Borealis.ps1").exists() or (parent / ".git").is_dir():
return parent
# Fallback to the ancestor that corresponds to ``<repo>/`` when the module
# lives under ``Data/Server/Modules``.
try:
return current.parents[4]
except IndexError:
return current.parent
@lru_cache(maxsize=None)
def server_runtime_root() -> Path:
"""Location where the running server stores mutable artefacts."""
env = _env_path("BOREALIS_SERVER_ROOT")
if env:
return env
root = project_root()
runtime = root / "Server" / "Borealis"
return runtime
def runtime_path(*parts: str) -> Path:
"""Return a path relative to the server runtime root."""
return server_runtime_root().joinpath(*parts)
def ensure_runtime_dir(*parts: str) -> Path:
"""Create (if required) and return a runtime directory."""
path = runtime_path(*parts)
path.mkdir(parents=True, exist_ok=True)
return path

View File

@@ -4,16 +4,20 @@ import react from '@vitejs/plugin-react';
import path from 'path'; import path from 'path';
import fs from 'fs'; import fs from 'fs';
const runtimeCertDir = process.env.BOREALIS_CERT_DIR;
const certCandidates = [ const certCandidates = [
process.env.BOREALIS_TLS_CERT, process.env.BOREALIS_TLS_CERT,
runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-cert.pem'),
path.resolve(__dirname, '../certs/borealis-server-cert.pem'), path.resolve(__dirname, '../certs/borealis-server-cert.pem'),
path.resolve(__dirname, '../../Data/Server/certs/borealis-server-cert.pem'), path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-cert.pem'),
] as const; ] as const;
const keyCandidates = [ const keyCandidates = [
process.env.BOREALIS_TLS_KEY, process.env.BOREALIS_TLS_KEY,
runtimeCertDir && path.resolve(runtimeCertDir, 'borealis-server-key.pem'),
path.resolve(__dirname, '../certs/borealis-server-key.pem'), path.resolve(__dirname, '../certs/borealis-server-key.pem'),
path.resolve(__dirname, '../../Data/Server/certs/borealis-server-key.pem'), path.resolve(__dirname, '../../../Server/Borealis/certs/borealis-server-key.pem'),
] as const; ] as const;
const pickFirst = (candidates: readonly (string | undefined)[]) => { const pickFirst = (candidates: readonly (string | undefined)[]) => {

View File

@@ -20,6 +20,8 @@ Section Guide:
import os import os
import sys import sys
from pathlib import Path
import ssl
# Ensure the modular server package is importable when the runtime is launched # Ensure the modular server package is importable when the runtime is launched
# from a packaged directory (e.g., Server/Borealis). We look for the canonical # from a packaged directory (e.g., Server/Borealis). We look for the canonical
@@ -40,6 +42,36 @@ eventlet.monkey_patch(thread=False)
from eventlet import tpool from eventlet import tpool
try:
from eventlet.wsgi import HttpProtocol # type: ignore
except Exception:
HttpProtocol = None # type: ignore[assignment]
else:
_original_handle_one_request = HttpProtocol.handle_one_request
def _quiet_tls_http_mismatch(self): # type: ignore[override]
try:
return _original_handle_one_request(self)
except ssl.SSLError as exc: # type: ignore[arg-type]
reason = getattr(exc, "reason", "")
reason_text = str(reason).lower() if reason else ""
message = " ".join(str(arg) for arg in exc.args if arg).lower()
if "http_request" in message or reason_text == "http request":
try:
self.close_connection = True # type: ignore[attr-defined]
except Exception:
pass
try:
conn = getattr(self, "socket", None) or getattr(self, "connection", None)
if conn:
conn.close()
except Exception:
pass
return None
raise
HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment]
import requests import requests
import re import re
import base64 import base64
@@ -167,6 +199,10 @@ TLS_CERT_PATH, TLS_KEY_PATH, TLS_BUNDLE_PATH = certificates.certificate_paths()
os.environ.setdefault("BOREALIS_TLS_CERT", TLS_CERT_PATH) os.environ.setdefault("BOREALIS_TLS_CERT", TLS_CERT_PATH)
os.environ.setdefault("BOREALIS_TLS_KEY", TLS_KEY_PATH) os.environ.setdefault("BOREALIS_TLS_KEY", TLS_KEY_PATH)
os.environ.setdefault("BOREALIS_TLS_BUNDLE", TLS_BUNDLE_PATH) os.environ.setdefault("BOREALIS_TLS_BUNDLE", TLS_BUNDLE_PATH)
try:
os.environ.setdefault("BOREALIS_CERT_DIR", str(Path(TLS_CERT_PATH).resolve().parent))
except Exception:
pass
JWT_SERVICE = jwt_service_module.load_service() JWT_SERVICE = jwt_service_module.load_service()
SCRIPT_SIGNER = signing.load_signer() SCRIPT_SIGNER = signing.load_signer()