More changes

This commit is contained in:
2025-10-19 07:21:28 -06:00
parent 84569e3e9a
commit e8760e3d85
8 changed files with 242 additions and 37 deletions

12
.gitignore vendored
View File

@@ -7,8 +7,11 @@ Borealis-Server.exe
# Production Deployment Folders # Production Deployment Folders
/Agent/ /Agent/
/Server/ /Server/
/Certificates/
/ElectronApp/ /ElectronApp/
/Logs/ /Logs/
/Temp/
database.db
# On-the-Fly Downloaded Dependencies # On-the-Fly Downloaded Dependencies
/Dependencies/NodeJS/ /Dependencies/NodeJS/
@@ -20,11 +23,4 @@ Borealis-Server.exe
# Misc Files/Folders # Misc Files/Folders
.vs/s .vs/s
__pycache__ __pycache__
/Agent/Python_API_Endpoints/__pycache__/ /Update_Staging/
/Update_Staging/
agent_settings.json
agent_settings_svc.json
agent_settings_user.json
users.json
database.db
/Temp/

View File

@@ -852,7 +852,7 @@ function InstallOrUpdate-BorealisAgent {
if (-not (Test-Path $serverUrlPath) -and (Test-Path $oldServerUrlPath)) { if (-not (Test-Path $serverUrlPath) -and (Test-Path $oldServerUrlPath)) {
try { Move-Item -Path $oldServerUrlPath -Destination $serverUrlPath -Force } catch { try { Copy-Item $oldServerUrlPath $serverUrlPath -Force } catch {} } try { Move-Item -Path $oldServerUrlPath -Destination $serverUrlPath -Force } catch { try { Copy-Item $oldServerUrlPath $serverUrlPath -Force } catch {} }
} }
$defaultUrl = 'http://localhost:5000' $defaultUrl = 'https://localhost:5000'
$currentUrl = $defaultUrl $currentUrl = $defaultUrl
if ($existingServerUrl -and $existingServerUrl.Trim()) { if ($existingServerUrl -and $existingServerUrl.Trim()) {
$currentUrl = $existingServerUrl.Trim() $currentUrl = $existingServerUrl.Trim()

View File

@@ -12,6 +12,7 @@ import platform
import stat import stat
import time import time
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import ssl import ssl
@@ -55,6 +56,49 @@ def _restrict_permissions(path: str) -> None:
pass pass
def _resolve_agent_certificate_dir(settings_dir: str, scope: str) -> str:
scope_name = (scope or "CURRENTUSER").strip().upper() or "CURRENTUSER"
def _as_path(value: Optional[str]) -> Optional[Path]:
if not value:
return None
try:
return Path(value).expanduser().resolve()
except Exception:
try:
return Path(value).expanduser()
except Exception:
return Path(value)
env_agent_root = _as_path(os.environ.get("BOREALIS_AGENT_CERT_ROOT"))
env_cert_root = _as_path(os.environ.get("BOREALIS_CERTIFICATES_ROOT")) or _as_path(
os.environ.get("BOREALIS_CERT_ROOT")
)
if env_agent_root is not None:
base = env_agent_root
elif env_cert_root is not None:
base = env_cert_root / "Agent"
else:
settings_path = Path(settings_dir).resolve()
try:
project_root = settings_path.parents[2]
except Exception:
project_root = settings_path.parent
base = project_root / "Certificates" / "Agent"
target = base / "Trusted_Server_Cert"
if scope_name not in {"SYSTEM", "CURRENTUSER"}:
target = target / scope_name
try:
target.mkdir(parents=True, exist_ok=True)
except Exception:
pass
return str(target)
class _FileLock: class _FileLock:
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None:
self.path = path self.path = path
@@ -226,15 +270,17 @@ class AgentIdentity:
class AgentKeyStore: class AgentKeyStore:
def __init__(self, settings_dir: str, scope: str = "CURRENTUSER") -> None: def __init__(self, settings_dir: str, scope: str = "CURRENTUSER") -> None:
self.settings_dir = settings_dir self.settings_dir = settings_dir
self.scope_system = scope.upper() == "SYSTEM" self.scope_name = (scope or "CURRENTUSER").strip().upper() or "CURRENTUSER"
self.scope_system = self.scope_name == "SYSTEM"
_ensure_dir(self.settings_dir) _ensure_dir(self.settings_dir)
self._certificate_dir = _resolve_agent_certificate_dir(self.settings_dir, self.scope_name)
self._private_path = os.path.join(self.settings_dir, "agent_key.ed25519") self._private_path = os.path.join(self.settings_dir, "agent_key.ed25519")
self._public_path = os.path.join(self.settings_dir, "agent_key.pub") self._public_path = os.path.join(self.settings_dir, "agent_key.pub")
self._guid_path = os.path.join(self.settings_dir, "guid.txt") self._guid_path = os.path.join(self.settings_dir, "guid.txt")
self._access_token_path = os.path.join(self.settings_dir, "access.jwt") self._access_token_path = os.path.join(self.settings_dir, "access.jwt")
self._refresh_token_path = os.path.join(self.settings_dir, "refresh.token") self._refresh_token_path = os.path.join(self.settings_dir, "refresh.token")
self._token_meta_path = os.path.join(self.settings_dir, "access.meta.json") self._token_meta_path = os.path.join(self.settings_dir, "access.meta.json")
self._server_certificate_path = os.path.join(self.settings_dir, "server_certificate.pem") self._server_certificate_path = os.path.join(self._certificate_dir, "server_certificate.pem")
self._server_signing_key_path = os.path.join(self.settings_dir, "server_signing_key.pub") self._server_signing_key_path = os.path.join(self.settings_dir, "server_signing_key.pub")
self._identity_lock_path = os.path.join(self.settings_dir, "identity.lock") self._identity_lock_path = os.path.join(self.settings_dir, "identity.lock")
self._installer_cache_path = os.path.join(self.settings_dir, "installer_code.shared.json") self._installer_cache_path = os.path.join(self.settings_dir, "installer_code.shared.json")

View File

@@ -141,6 +141,18 @@ def register(
# Another device already claims this hostname; keep the existing # Another device already claims this hostname; keep the existing
# canonical hostname assigned during enrollment to avoid breaking # canonical hostname assigned during enrollment to avoid breaking
# the unique constraint and continue updating the remaining fields. # the unique constraint and continue updating the remaining fields.
existing_guid_for_hostname: Optional[str] = None
if "hostname" in updates:
try:
cur.execute(
"SELECT guid FROM devices WHERE hostname = ?",
(updates["hostname"],),
)
row = cur.fetchone()
if row and row[0]:
existing_guid_for_hostname = normalize_guid(row[0])
except Exception:
existing_guid_for_hostname = None
if "hostname" in updates: if "hostname" in updates:
updates.pop("hostname", None) updates.pop("hostname", None)
try: try:
@@ -148,12 +160,23 @@ def register(
except sqlite3.IntegrityError: except sqlite3.IntegrityError:
raise raise
else: else:
log( try:
"server", current_guid = normalize_guid(ctx.guid)
"heartbeat hostname collision ignored for guid=" except Exception:
f"{ctx.guid}", current_guid = ctx.guid
context_label, if (
) existing_guid_for_hostname
and current_guid
and existing_guid_for_hostname == current_guid
):
pass # Same device contexts; no log needed.
else:
log(
"server",
"heartbeat hostname collision ignored for guid="
f"{ctx.guid}",
context_label,
)
else: else:
raise raise

View File

@@ -19,13 +19,18 @@ 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
from Modules.runtime import ensure_runtime_dir, runtime_path from Modules.runtime import ensure_server_certificates_dir, server_certificates_path, runtime_path
_CERT_DIR = runtime_path("certs") _CERT_DIR = server_certificates_path()
_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"
_LEGACY_CERT_DIR = runtime_path("certs")
_LEGACY_CERT_FILE = _LEGACY_CERT_DIR / "borealis-server-cert.pem"
_LEGACY_KEY_FILE = _LEGACY_CERT_DIR / "borealis-server-key.pem"
_LEGACY_BUNDLE_FILE = _LEGACY_CERT_DIR / "borealis-server-bundle.pem"
# 100-year lifetime (effectively "never" for self-signed deployments). # 100-year lifetime (effectively "never" for self-signed deployments).
_CERT_VALIDITY = timedelta(days=365 * 100) _CERT_VALIDITY = timedelta(days=365 * 100)
@@ -37,7 +42,8 @@ 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).
""" """
ensure_runtime_dir("certs") ensure_server_certificates_dir()
_migrate_legacy_material_if_present()
regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists()) regenerate = not (_CERT_FILE.exists() and _KEY_FILE.exists())
if not regenerate: if not regenerate:
@@ -62,6 +68,38 @@ def ensure_certificate(common_name: str = "Borealis Server") -> Tuple[Path, Path
return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE
def _migrate_legacy_material_if_present() -> None:
if _CERT_FILE.exists() and _KEY_FILE.exists():
return
legacy_cert = _LEGACY_CERT_FILE
legacy_key = _LEGACY_KEY_FILE
legacy_bundle = _LEGACY_BUNDLE_FILE
if not legacy_cert.exists() or not legacy_key.exists():
return
try:
ensure_server_certificates_dir()
if not _CERT_FILE.exists():
try:
legacy_cert.replace(_CERT_FILE)
except Exception:
_CERT_FILE.write_bytes(legacy_cert.read_bytes())
if not _KEY_FILE.exists():
try:
legacy_key.replace(_KEY_FILE)
except Exception:
_KEY_FILE.write_bytes(legacy_key.read_bytes())
if legacy_bundle.exists() and not _BUNDLE_FILE.exists():
try:
legacy_bundle.replace(_BUNDLE_FILE)
except Exception:
_BUNDLE_FILE.write_bytes(legacy_bundle.read_bytes())
except Exception:
return
def _generate_certificate(common_name: str) -> None: def _generate_certificate(common_name: str) -> None:
private_key = ec.generate_private_key(ec.SECP384R1()) private_key = ec.generate_private_key(ec.SECP384R1())
public_key = private_key.public_key() public_key = private_key.public_key()

View File

@@ -10,15 +10,22 @@ 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 Modules.runtime import (
ensure_server_certificates_dir,
server_certificates_path,
runtime_path,
)
from .keys import base64_from_spki_der from .keys import base64_from_spki_der
_KEY_DIR = runtime_path("script_signing_keys") _KEY_DIR = server_certificates_path("Code-Signing")
_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_KEY_FILE = runtime_path("keys") / "borealis-script-ed25519.key"
_LEGACY_PUB_FILE = runtime_path("keys") / "borealis-script-ed25519.pub" _LEGACY_PUB_FILE = runtime_path("keys") / "borealis-script-ed25519.pub"
_OLD_RUNTIME_KEY_DIR = runtime_path("script_signing_keys")
_OLD_RUNTIME_KEY_FILE = _OLD_RUNTIME_KEY_DIR / "borealis-script-ed25519.key"
_OLD_RUNTIME_PUB_FILE = _OLD_RUNTIME_KEY_DIR / "borealis-script-ed25519.pub"
class ScriptSigner: class ScriptSigner:
@@ -45,7 +52,7 @@ def load_signer() -> ScriptSigner:
def _load_or_create() -> ed25519.Ed25519PrivateKey: def _load_or_create() -> ed25519.Ed25519PrivateKey:
ensure_runtime_dir("script_signing_keys") ensure_server_certificates_dir("Code-Signing")
_migrate_legacy_material_if_present() _migrate_legacy_material_if_present()
if _SIGNING_KEY_FILE.exists(): if _SIGNING_KEY_FILE.exists():
@@ -80,11 +87,30 @@ def _load_or_create() -> ed25519.Ed25519PrivateKey:
def _migrate_legacy_material_if_present() -> None: def _migrate_legacy_material_if_present() -> None:
if _SIGNING_KEY_FILE.exists():
return
# First migrate from legacy runtime path embedded in Server runtime.
try:
if _OLD_RUNTIME_KEY_FILE.exists() and not _SIGNING_KEY_FILE.exists():
ensure_server_certificates_dir("Code-Signing")
try:
_OLD_RUNTIME_KEY_FILE.replace(_SIGNING_KEY_FILE)
except Exception:
_SIGNING_KEY_FILE.write_bytes(_OLD_RUNTIME_KEY_FILE.read_bytes())
if _OLD_RUNTIME_PUB_FILE.exists() and not _SIGNING_PUB_FILE.exists():
try:
_OLD_RUNTIME_PUB_FILE.replace(_SIGNING_PUB_FILE)
except Exception:
_SIGNING_PUB_FILE.write_bytes(_OLD_RUNTIME_PUB_FILE.read_bytes())
except Exception:
pass
if not _LEGACY_KEY_FILE.exists() or _SIGNING_KEY_FILE.exists(): if not _LEGACY_KEY_FILE.exists() or _SIGNING_KEY_FILE.exists():
return return
try: try:
ensure_runtime_dir("script_signing_keys") ensure_server_certificates_dir("Code-Signing")
try: try:
_LEGACY_KEY_FILE.replace(_SIGNING_KEY_FILE) _LEGACY_KEY_FILE.replace(_SIGNING_KEY_FILE)
except Exception: except Exception:
@@ -97,4 +123,3 @@ def _migrate_legacy_material_if_present() -> None:
_SIGNING_PUB_FILE.write_bytes(_LEGACY_PUB_FILE.read_bytes()) _SIGNING_PUB_FILE.write_bytes(_LEGACY_PUB_FILE.read_bytes())
except Exception: except Exception:
return return

View File

@@ -82,13 +82,47 @@ def ensure_runtime_dir(*parts: str) -> Path:
def certificates_root() -> Path: def certificates_root() -> Path:
"""Base directory for persisted certificate material.""" """Base directory for persisted certificate material."""
env = _env_path("BOREALIS_CERT_ROOT") env = _env_path("BOREALIS_CERTIFICATES_ROOT") or _env_path("BOREALIS_CERT_ROOT")
if env: if env:
env.mkdir(parents=True, exist_ok=True) env.mkdir(parents=True, exist_ok=True)
return env return env
root = project_root() / "Certificates" root = project_root() / "Certificates"
root.mkdir(parents=True, exist_ok=True) root.mkdir(parents=True, exist_ok=True)
# Ensure expected subdirectories exist for agent and server material.
try:
(root / "Server").mkdir(parents=True, exist_ok=True)
(root / "Agent").mkdir(parents=True, exist_ok=True)
except Exception:
pass
return root
@lru_cache(maxsize=None)
def server_certificates_root() -> Path:
"""Base directory for server certificate material."""
env = _env_path("BOREALIS_SERVER_CERT_ROOT")
if env:
env.mkdir(parents=True, exist_ok=True)
return env
root = certificates_root() / "Server"
root.mkdir(parents=True, exist_ok=True)
return root
@lru_cache(maxsize=None)
def agent_certificates_root() -> Path:
"""Base directory for agent certificate material."""
env = _env_path("BOREALIS_AGENT_CERT_ROOT")
if env:
env.mkdir(parents=True, exist_ok=True)
return env
root = certificates_root() / "Agent"
root.mkdir(parents=True, exist_ok=True)
return root return root
@@ -104,3 +138,31 @@ def ensure_certificates_dir(*parts: str) -> Path:
path = certificates_path(*parts) path = certificates_path(*parts)
path.mkdir(parents=True, exist_ok=True) path.mkdir(parents=True, exist_ok=True)
return path return path
def server_certificates_path(*parts: str) -> Path:
"""Return a path under the server certificates root."""
return server_certificates_root().joinpath(*parts)
def ensure_server_certificates_dir(*parts: str) -> Path:
"""Create (if required) and return a server certificates subdirectory."""
path = server_certificates_path(*parts)
path.mkdir(parents=True, exist_ok=True)
return path
def agent_certificates_path(*parts: str) -> Path:
"""Return a path under the agent certificates root."""
return agent_certificates_root().joinpath(*parts)
def ensure_agent_certificates_dir(*parts: str) -> Path:
"""Create (if required) and return an agent certificates subdirectory."""
path = agent_certificates_path(*parts)
path.mkdir(parents=True, exist_ok=True)
return path

View File

@@ -50,25 +50,40 @@ else:
_original_handle_one_request = HttpProtocol.handle_one_request _original_handle_one_request = HttpProtocol.handle_one_request
def _quiet_tls_http_mismatch(self): # type: ignore[override] def _quiet_tls_http_mismatch(self): # type: ignore[override]
def _close_connection_quietly():
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
try: try:
return _original_handle_one_request(self) return _original_handle_one_request(self)
except ssl.SSLError as exc: # type: ignore[arg-type] except ssl.SSLError as exc: # type: ignore[arg-type]
reason = getattr(exc, "reason", "") reason = getattr(exc, "reason", "")
reason_text = str(reason).lower() if reason else "" reason_text = str(reason).lower() if reason else ""
message = " ".join(str(arg) for arg in exc.args if arg).lower() message = " ".join(str(arg) for arg in exc.args if arg).lower()
if "http_request" in message or reason_text == "http request": if (
try: "http_request" in message
self.close_connection = True # type: ignore[attr-defined] or reason_text == "http request"
except Exception: or "unknown ca" in message
pass or reason_text == "unknown ca"
try: or "unknown_ca" in message
conn = getattr(self, "socket", None) or getattr(self, "connection", None) ):
if conn: _close_connection_quietly()
conn.close()
except Exception:
pass
return None return None
raise raise
except ssl.SSLEOFError:
_close_connection_quietly()
return None
except ConnectionAbortedError:
_close_connection_quietly()
return None
HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment] HttpProtocol.handle_one_request = _quiet_tls_http_mismatch # type: ignore[assignment]