mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-27 04:41:58 -06:00
Fix agent keystore initialization order
This commit is contained in:
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
78
Data/Server/Modules/runtime.py
Normal file
78
Data/Server/Modules/runtime.py
Normal 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
|
||||||
@@ -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)[]) => {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user