Add TLS bootstrap support for Engine runtime

This commit is contained in:
2025-10-22 18:17:35 -06:00
parent 4b18c485b0
commit 7aa6474a6d
6 changed files with 602 additions and 20 deletions

View File

@@ -52,6 +52,26 @@ The Engine mirrors the legacy defaults so it can boot without additional configu
| `BOREALIS_REPO_BRANCH` | Default branch tracked by the Engine GitHub integration. | `main` |
| `BOREALIS_REPO_HASH_REFRESH` | Seconds between default repository head refresh attempts (clamped 30-3600). | `60` |
| `BOREALIS_CACHE_DIR` | Directory used to persist Engine cache files (GitHub repo head cache). | `<project_root>/Data/Engine/cache` |
| `BOREALIS_CERTIFICATES_ROOT` | Overrides where TLS certificates (root CA + leaf) are stored. | `<project_root>/Certificates` |
| `BOREALIS_SERVER_CERT_ROOT` | Directly points to the Engine server certificate directory if certificates are staged elsewhere. | `<project_root>/Certificates/Server` |
## TLS and transport stack
`Data/Engine/services/crypto/certificates.py` mirrors the legacy certificate
generator so the Engine always serves HTTPS with a self-managed root CA and
leaf certificate. During bootstrap the Engine:
1. Runs the certificate helper to ensure the root CA, server key, and bundle
exist under `Certificates/Server/` (or the configured override path).
2. Exposes the resulting bundle via `BOREALIS_TLS_BUNDLE` so enrollment flows
can deliver the pinned certificate to agents.
3. Launches Socket.IO/Eventlet with the generated cert/key pair. A fallback to
Werkzeugs TLS support keeps HTTPS available even if Socket.IO is disabled.
`Data/Engine/interfaces/eventlet_compat.py` applies the same Eventlet monkey
patch as the legacy server so TLS handshakes presented to the HTTP listener are
handled quietly instead of surfacing `400 Bad Request` noise when non-TLS
clients connect.
## Logging expectations
@@ -166,6 +186,8 @@ The suite currently validates:
malformed requests.
- SQLite schema migrations to ensure the Engine can provision required tables in
a fresh database.
- TLS certificate provisioning helpers to guarantee HTTPS material exists before
the Engine starts serving requests.
Successful execution prints a summary similar to:

View File

@@ -2,7 +2,9 @@
from __future__ import annotations
import os
from dataclasses import dataclass
from pathlib import Path
from typing import Optional
from flask import Flask
@@ -13,10 +15,15 @@ from .interfaces import (
register_http_interfaces,
register_ws_interfaces,
)
from .interfaces.eventlet_compat import apply_eventlet_patches
from .repositories.sqlite import connection as sqlite_connection
from .repositories.sqlite import migrations as sqlite_migrations
from .server import create_app
from .services.container import build_service_container
from .services.crypto.certificates import ensure_certificate
apply_eventlet_patches()
@dataclass(frozen=True, slots=True)
@@ -27,6 +34,9 @@ class EngineRuntime:
settings: EngineSettings
socketio: Optional[object]
db_factory: sqlite_connection.SQLiteConnectionFactory
tls_certificate: Path
tls_key: Path
tls_bundle: Path
def bootstrap() -> EngineRuntime:
@@ -36,6 +46,17 @@ def bootstrap() -> EngineRuntime:
logger = configure_logging(settings)
logger.info("bootstrap-started")
cert_path, key_path, bundle_path = ensure_certificate()
os.environ.setdefault("BOREALIS_TLS_BUNDLE", str(bundle_path))
logger.info(
"tls-material-ready",
extra={
"cert_path": str(cert_path),
"key_path": str(key_path),
"bundle_path": str(bundle_path),
},
)
db_factory = sqlite_connection.connection_factory(settings.database_path)
if settings.apply_migrations:
logger.info("migrations-start")
@@ -53,24 +74,38 @@ def bootstrap() -> EngineRuntime:
register_ws_interfaces(socketio, services)
services.scheduler_service.start(socketio)
logger.info("bootstrap-complete")
return EngineRuntime(app=app, settings=settings, socketio=socketio, db_factory=db_factory)
return EngineRuntime(
app=app,
settings=settings,
socketio=socketio,
db_factory=db_factory,
tls_certificate=cert_path,
tls_key=key_path,
tls_bundle=bundle_path,
)
def main() -> None:
runtime = bootstrap()
socketio = runtime.socketio
certfile = str(runtime.tls_bundle)
keyfile = str(runtime.tls_key)
if socketio is not None:
socketio.run( # type: ignore[call-arg]
runtime.app,
host=runtime.settings.server.host,
port=runtime.settings.server.port,
debug=runtime.settings.debug,
certfile=certfile,
keyfile=keyfile,
)
else:
runtime.app.run(
host=runtime.settings.server.host,
port=runtime.settings.server.port,
debug=runtime.settings.debug,
ssl_context=(certfile, keyfile),
)

View File

@@ -0,0 +1,75 @@
"""Compatibility helpers for running Socket.IO under eventlet."""
from __future__ import annotations
import ssl
from typing import Any
try: # pragma: no cover - optional dependency
import eventlet # type: ignore
except Exception: # pragma: no cover - optional dependency
eventlet = None # type: ignore[assignment]
def _quiet_close(connection: Any) -> None:
try:
if hasattr(connection, "close"):
connection.close()
except Exception:
pass
def _close_connection_quietly(protocol: Any) -> None:
try:
setattr(protocol, "close_connection", True)
except Exception:
pass
conn = getattr(protocol, "socket", None) or getattr(protocol, "connection", None)
if conn is not None:
_quiet_close(conn)
def apply_eventlet_patches() -> None:
"""Apply Borealis-specific eventlet tweaks when the dependency is available."""
if eventlet is None: # pragma: no cover - guard for environments without eventlet
return
eventlet.monkey_patch(thread=False)
try:
from eventlet.wsgi import HttpProtocol # type: ignore
except Exception: # pragma: no cover - import guard
return
original = HttpProtocol.handle_one_request # type: ignore[attr-defined]
def _handle_one_request(self: Any, *args: Any, **kwargs: Any) -> Any: # type: ignore[override]
try:
return original(self, *args, **kwargs)
except ssl.SSLError as exc: # type: ignore[arg-type]
reason = getattr(exc, "reason", "") or ""
message = " ".join(str(arg) for arg in exc.args if arg)
lower_reason = str(reason).lower()
lower_message = message.lower()
if (
"http_request" in lower_message
or lower_reason == "http request"
or "unknown ca" in lower_message
or lower_reason == "unknown ca"
or "unknown_ca" in lower_message
):
_close_connection_quietly(self)
return None
raise
except ssl.SSLEOFError:
_close_connection_quietly(self)
return None
except ConnectionAbortedError:
_close_connection_quietly(self)
return None
HttpProtocol.handle_one_request = _handle_one_request # type: ignore[assignment]
__all__ = ["apply_eventlet_patches"]

View File

@@ -2,25 +2,8 @@
from __future__ import annotations
from .auth import (
DeviceAuthService,
DeviceRecord,
RefreshTokenRecord,
TokenRefreshError,
TokenRefreshErrorCode,
TokenService,
)
from .enrollment import (
EnrollmentRequestResult,
EnrollmentService,
EnrollmentStatus,
EnrollmentTokenBundle,
PollingResult,
)
from Data.Engine.domain.device_enrollment import EnrollmentValidationError
from .jobs.scheduler_service import SchedulerService
from .github import GitHubService, GitHubTokenPayload
from .realtime import AgentRealtimeService, AgentRecord
from importlib import import_module
from typing import Any, Dict, Tuple
__all__ = [
"DeviceAuthService",
@@ -41,3 +24,39 @@ __all__ = [
"GitHubService",
"GitHubTokenPayload",
]
_LAZY_TARGETS: Dict[str, Tuple[str, str]] = {
"DeviceAuthService": ("Data.Engine.services.auth.device_auth_service", "DeviceAuthService"),
"DeviceRecord": ("Data.Engine.services.auth.device_auth_service", "DeviceRecord"),
"RefreshTokenRecord": ("Data.Engine.services.auth.device_auth_service", "RefreshTokenRecord"),
"TokenService": ("Data.Engine.services.auth.token_service", "TokenService"),
"TokenRefreshError": ("Data.Engine.services.auth.token_service", "TokenRefreshError"),
"TokenRefreshErrorCode": ("Data.Engine.services.auth.token_service", "TokenRefreshErrorCode"),
"EnrollmentService": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentService"),
"EnrollmentRequestResult": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentRequestResult"),
"EnrollmentStatus": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentStatus"),
"EnrollmentTokenBundle": ("Data.Engine.services.enrollment.enrollment_service", "EnrollmentTokenBundle"),
"PollingResult": ("Data.Engine.services.enrollment.enrollment_service", "PollingResult"),
"EnrollmentValidationError": ("Data.Engine.domain.device_enrollment", "EnrollmentValidationError"),
"AgentRealtimeService": ("Data.Engine.services.realtime.agent_registry", "AgentRealtimeService"),
"AgentRecord": ("Data.Engine.services.realtime.agent_registry", "AgentRecord"),
"SchedulerService": ("Data.Engine.services.jobs.scheduler_service", "SchedulerService"),
"GitHubService": ("Data.Engine.services.github.github_service", "GitHubService"),
"GitHubTokenPayload": ("Data.Engine.services.github.github_service", "GitHubTokenPayload"),
}
def __getattr__(name: str) -> Any:
try:
module_name, attribute = _LAZY_TARGETS[name]
except KeyError as exc:
raise AttributeError(name) from exc
module = import_module(module_name)
value = getattr(module, attribute)
globals()[name] = value
return value
def __dir__() -> Any: # pragma: no cover - interactive helper
return sorted(set(__all__))

View File

@@ -0,0 +1,366 @@
"""Server TLS certificate management for the Borealis Engine."""
from __future__ import annotations
import os
import ssl
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Optional, Tuple
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
from Data.Engine.runtime import ensure_server_certificates_dir, runtime_path, server_certificates_path
__all__ = [
"build_ssl_context",
"certificate_paths",
"ensure_certificate",
]
_CERT_DIR = server_certificates_path()
_CERT_FILE = _CERT_DIR / "borealis-server-cert.pem"
_KEY_FILE = _CERT_DIR / "borealis-server-key.pem"
_BUNDLE_FILE = _CERT_DIR / "borealis-server-bundle.pem"
_CA_KEY_FILE = _CERT_DIR / "borealis-root-ca-key.pem"
_CA_CERT_FILE = _CERT_DIR / "borealis-root-ca.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"
_ROOT_COMMON_NAME = "Borealis Root CA"
_ORG_NAME = "Borealis"
_ROOT_VALIDITY = timedelta(days=365 * 100)
_SERVER_VALIDITY = timedelta(days=365 * 5)
def ensure_certificate(common_name: str = "Borealis Engine") -> Tuple[Path, Path, Path]:
"""Ensure the root CA, server certificate, and bundle exist on disk."""
ensure_server_certificates_dir()
_migrate_legacy_material_if_present()
ca_key, ca_cert, ca_regenerated = _ensure_root_ca()
server_cert = _load_certificate(_CERT_FILE)
needs_regen = ca_regenerated or _server_certificate_needs_regeneration(server_cert, ca_cert)
if needs_regen:
server_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
if server_cert is None:
server_cert = _generate_server_certificate(common_name, ca_key, ca_cert)
_write_bundle(server_cert, ca_cert)
return _CERT_FILE, _KEY_FILE, _BUNDLE_FILE
def _migrate_legacy_material_if_present() -> None:
if not _CERT_FILE.exists() or not _KEY_FILE.exists():
legacy_cert = _LEGACY_CERT_FILE
legacy_key = _LEGACY_KEY_FILE
if legacy_cert.exists() and legacy_key.exists():
try:
ensure_server_certificates_dir()
if not _CERT_FILE.exists():
_safe_copy(legacy_cert, _CERT_FILE)
if not _KEY_FILE.exists():
_safe_copy(legacy_key, _KEY_FILE)
except Exception:
pass
def _ensure_root_ca() -> Tuple[ec.EllipticCurvePrivateKey, x509.Certificate, bool]:
regenerated = False
ca_key: Optional[ec.EllipticCurvePrivateKey] = None
ca_cert: Optional[x509.Certificate] = None
if _CA_KEY_FILE.exists() and _CA_CERT_FILE.exists():
try:
ca_key = _load_private_key(_CA_KEY_FILE)
ca_cert = _load_certificate(_CA_CERT_FILE)
if ca_cert is not None and ca_key is not None:
expiry = _cert_not_after(ca_cert)
subject = ca_cert.subject
subject_cn = ""
try:
subject_cn = subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value # type: ignore[index]
except Exception:
subject_cn = ""
try:
basic = ca_cert.extensions.get_extension_for_class(x509.BasicConstraints).value # type: ignore[attr-defined]
is_ca = bool(basic.ca)
except Exception:
is_ca = False
if (
expiry <= datetime.now(tz=timezone.utc)
or not is_ca
or subject_cn != _ROOT_COMMON_NAME
):
regenerated = True
else:
regenerated = True
except Exception:
regenerated = True
else:
regenerated = True
if regenerated or ca_key is None or ca_cert is None:
ca_key = ec.generate_private_key(ec.SECP384R1())
public_key = ca_key.public_key()
now = datetime.now(tz=timezone.utc)
builder = (
x509.CertificateBuilder()
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, _ROOT_COMMON_NAME),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
]
)
)
.issuer_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, _ROOT_COMMON_NAME),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
]
)
)
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(minutes=5))
.not_valid_after(now + _ROOT_VALIDITY)
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=True,
crl_sign=True,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(public_key),
critical=False,
)
)
builder = builder.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(public_key),
critical=False,
)
ca_cert = builder.sign(private_key=ca_key, algorithm=hashes.SHA384())
_CA_KEY_FILE.write_bytes(
ca_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
_CA_CERT_FILE.write_bytes(ca_cert.public_bytes(serialization.Encoding.PEM))
_tighten_permissions(_CA_KEY_FILE)
_tighten_permissions(_CA_CERT_FILE)
else:
regenerated = False
return ca_key, ca_cert, regenerated
def _server_certificate_needs_regeneration(
server_cert: Optional[x509.Certificate],
ca_cert: x509.Certificate,
) -> bool:
if server_cert is None:
return True
try:
if server_cert.issuer != ca_cert.subject:
return True
except Exception:
return True
try:
expiry = _cert_not_after(server_cert)
if expiry <= datetime.now(tz=timezone.utc):
return True
except Exception:
return True
try:
basic = server_cert.extensions.get_extension_for_class(x509.BasicConstraints).value # type: ignore[attr-defined]
if basic.ca:
return True
except Exception:
return True
try:
eku = server_cert.extensions.get_extension_for_class(x509.ExtendedKeyUsage).value # type: ignore[attr-defined]
if ExtendedKeyUsageOID.SERVER_AUTH not in eku:
return True
except Exception:
return True
return False
def _generate_server_certificate(
common_name: str,
ca_key: ec.EllipticCurvePrivateKey,
ca_cert: x509.Certificate,
) -> x509.Certificate:
private_key = ec.generate_private_key(ec.SECP384R1())
public_key = private_key.public_key()
now = datetime.now(tz=timezone.utc)
ca_expiry = _cert_not_after(ca_cert)
candidate_expiry = now + _SERVER_VALIDITY
not_after = min(ca_expiry - timedelta(days=1), candidate_expiry)
builder = (
x509.CertificateBuilder()
.subject_name(
x509.Name(
[
x509.NameAttribute(NameOID.COMMON_NAME, common_name),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, _ORG_NAME),
]
)
)
.issuer_name(ca_cert.subject)
.public_key(public_key)
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(minutes=5))
.not_valid_after(not_after)
.add_extension(
x509.SubjectAlternativeName(
[
x509.DNSName("localhost"),
x509.DNSName("127.0.0.1"),
x509.DNSName("::1"),
]
),
critical=False,
)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=False,
data_encipherment=False,
key_agreement=False,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
critical=True,
)
.add_extension(
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]),
critical=False,
)
.add_extension(
x509.SubjectKeyIdentifier.from_public_key(public_key),
critical=False,
)
.add_extension(
x509.AuthorityKeyIdentifier.from_issuer_public_key(ca_key.public_key()),
critical=False,
)
)
certificate = builder.sign(private_key=ca_key, algorithm=hashes.SHA384())
_KEY_FILE.write_bytes(
private_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
_CERT_FILE.write_bytes(certificate.public_bytes(serialization.Encoding.PEM))
_tighten_permissions(_KEY_FILE)
_tighten_permissions(_CERT_FILE)
return certificate
def _write_bundle(server_cert: x509.Certificate, ca_cert: x509.Certificate) -> None:
try:
server_pem = server_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip()
ca_pem = ca_cert.public_bytes(serialization.Encoding.PEM).decode("utf-8").strip()
except Exception:
return
bundle = f"{server_pem}\n{ca_pem}\n"
_BUNDLE_FILE.write_text(bundle, encoding="utf-8")
_tighten_permissions(_BUNDLE_FILE)
def _safe_copy(src: Path, dst: Path) -> None:
try:
dst.write_bytes(src.read_bytes())
except Exception:
pass
def _tighten_permissions(path: Path) -> None:
try:
if os.name == "posix":
path.chmod(0o600)
except Exception:
pass
def _load_private_key(path: Path) -> ec.EllipticCurvePrivateKey:
with path.open("rb") as fh:
return serialization.load_pem_private_key(fh.read(), password=None)
def _load_certificate(path: Path) -> Optional[x509.Certificate]:
try:
return x509.load_pem_x509_certificate(path.read_bytes())
except Exception:
return None
def _cert_not_after(cert: x509.Certificate) -> datetime:
try:
return cert.not_valid_after_utc # type: ignore[attr-defined]
except AttributeError:
value = cert.not_valid_after
if value.tzinfo is None:
return value.replace(tzinfo=timezone.utc)
return value
def build_ssl_context() -> ssl.SSLContext:
cert_path, key_path, bundle_path = ensure_certificate()
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.minimum_version = ssl.TLSVersion.TLSv1_3
context.load_cert_chain(certfile=str(bundle_path), keyfile=str(key_path))
return context
def certificate_paths() -> Tuple[str, str, str]:
cert_path, key_path, bundle_path = ensure_certificate()
return str(cert_path), str(key_path), str(bundle_path)

View File

@@ -0,0 +1,65 @@
from __future__ import annotations
import importlib
import os
import shutil
import ssl
import sys
import tempfile
import unittest
from pathlib import Path
from Data.Engine import runtime
class CertificateGenerationTests(unittest.TestCase):
def setUp(self) -> None:
self._tmpdir = Path(tempfile.mkdtemp(prefix="engine-cert-tests-"))
self.addCleanup(lambda: shutil.rmtree(self._tmpdir, ignore_errors=True))
self._previous_env: dict[str, str | None] = {}
for name in ("BOREALIS_CERTIFICATES_ROOT", "BOREALIS_SERVER_CERT_ROOT"):
self._previous_env[name] = os.environ.get(name)
os.environ[name] = str(self._tmpdir / name.lower())
runtime.certificates_root.cache_clear()
runtime.server_certificates_root.cache_clear()
module_name = "Data.Engine.services.crypto.certificates"
if module_name in sys.modules:
del sys.modules[module_name]
try:
self.certificates = importlib.import_module(module_name)
except ModuleNotFoundError as exc: # pragma: no cover - optional deps absent
self.skipTest(f"cryptography dependency unavailable: {exc}")
def tearDown(self) -> None: # pragma: no cover - environment cleanup
for name, value in self._previous_env.items():
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value
runtime.certificates_root.cache_clear()
runtime.server_certificates_root.cache_clear()
def test_ensure_certificate_creates_material(self) -> None:
cert_path, key_path, bundle_path = self.certificates.ensure_certificate()
self.assertTrue(cert_path.exists(), "certificate was not generated")
self.assertTrue(key_path.exists(), "private key was not generated")
self.assertTrue(bundle_path.exists(), "bundle was not generated")
context = self.certificates.build_ssl_context()
self.assertIsInstance(context, ssl.SSLContext)
self.assertEqual(context.minimum_version, ssl.TLSVersion.TLSv1_3)
def test_certificate_paths_returns_strings(self) -> None:
cert_path, key_path, bundle_path = self.certificates.certificate_paths()
self.assertIsInstance(cert_path, str)
self.assertIsInstance(key_path, str)
self.assertIsInstance(bundle_path, str)
if __name__ == "__main__": # pragma: no cover - convenience
unittest.main()