mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-10-26 13:01:58 -06:00
Fixed Inventory Role Authentication
This commit is contained in:
@@ -51,7 +51,7 @@ Today the stable core focuses on workflow-driven API and automation scenarios. R
|
||||
Agents establish TLS-secured REST calls to the Flask backend on port 5000 and keep an authenticated WebSocket session for interactive features such as screenshot capture. Future plans include WebRTC for higher-performance remote desktop. Every agent now performs an enrollment handshake (see **Secure Enrollment & Tokens** below) prior to opening either channel; all API access is bound to short-lived Ed25519-signed JWTs.
|
||||
|
||||
### Secure Enrollment & Tokens
|
||||
- On first launch the agent generates an Ed25519 identity and stores the private key under `Agent/Borealis/Settings/agent_key.ed25519` (protected with DPAPI on Windows or chmod 600 elsewhere). The public key is retained as SPKI DER and fingerprinted with SHA-256.
|
||||
- On first launch the agent generates an Ed25519 identity and stores the private key under `Certificates/Agent/Identity/<Context>/agent_identity_private.ed25519` (protected with DPAPI on Windows or chmod 600 elsewhere). The public key is retained alongside it as Base64 (`agent_identity_public.ed25519`) and fingerprinted with SHA-256.
|
||||
- Enrollment starts with an installer code (minted in the Web UI) and proves key possession by signing the server nonce. Upon operator approval the server issues:
|
||||
- The canonical device GUID (persisted to `guid.txt` alongside the key material).
|
||||
- A short-lived access token (EdDSA/JWT) and a long-lived refresh token (stored encrypted via DPAPI and hashed server-side).
|
||||
@@ -204,4 +204,3 @@ This section summarizes what is considered usable vs. experimental today.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -983,13 +983,30 @@ class Role:
|
||||
# Always post the latest available details (possibly cached)
|
||||
details_to_send = self._last_details or {'summary': collect_summary(self.ctx.config)}
|
||||
get_url = (self.ctx.hooks.get('get_server_url') if isinstance(self.ctx.hooks, dict) else None) or (lambda: 'http://localhost:5000')
|
||||
url = (get_url() or '').rstrip('/') + '/api/agent/details'
|
||||
payload = {
|
||||
'agent_id': self.ctx.agent_id,
|
||||
'hostname': details_to_send.get('summary', {}).get('hostname', socket.gethostname()),
|
||||
'details': details_to_send,
|
||||
}
|
||||
client_factory = None
|
||||
if isinstance(self.ctx.hooks, dict):
|
||||
client_factory = self.ctx.hooks.get('http_client')
|
||||
|
||||
if callable(client_factory):
|
||||
try:
|
||||
client = client_factory()
|
||||
except Exception:
|
||||
client = None
|
||||
else:
|
||||
try:
|
||||
await client.async_post_json("/api/agent/details", payload, require_auth=True)
|
||||
await asyncio.sleep(interval_sec)
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if aiohttp is not None:
|
||||
url = (get_url() or '').rstrip('/') + '/api/agent/details'
|
||||
async with aiohttp.ClientSession() as session:
|
||||
await session.post(url, json=payload, timeout=10)
|
||||
except Exception:
|
||||
|
||||
@@ -236,9 +236,9 @@ INSTALLER_CODE_OVERRIDE = (
|
||||
def _agent_guid_path() -> str:
|
||||
try:
|
||||
root = _find_project_root()
|
||||
return os.path.join(root, 'Agent', 'Borealis', 'agent_GUID')
|
||||
return os.path.join(root, 'Agent', 'Borealis', 'Settings', 'Agent_GUID.txt')
|
||||
except Exception:
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'agent_GUID'))
|
||||
return os.path.abspath(os.path.join(os.path.dirname(__file__), 'Settings', 'Agent_GUID.txt'))
|
||||
|
||||
|
||||
def _settings_dir():
|
||||
@@ -3068,7 +3068,11 @@ if __name__=='__main__':
|
||||
_log_agent(f'Authentication bootstrap failed: {exc}', fname='agent.error.log')
|
||||
print(f"[WARN] Authentication bootstrap failed: {exc}")
|
||||
try:
|
||||
base_hooks = {'send_service_control': send_service_control, 'get_server_url': get_server_url}
|
||||
base_hooks = {
|
||||
'send_service_control': send_service_control,
|
||||
'get_server_url': get_server_url,
|
||||
'http_client': http_client,
|
||||
}
|
||||
if not SYSTEM_SERVICE_MODE:
|
||||
# Load interactive-context roles (tray/UI, current-user execution, screenshot, etc.)
|
||||
hooks_interactive = {**base_hooks, 'service_mode': 'currentuser'}
|
||||
|
||||
@@ -99,6 +99,51 @@ def _resolve_agent_certificate_dir(settings_dir: str, scope: str) -> str:
|
||||
return str(target)
|
||||
|
||||
|
||||
def _resolve_agent_identity_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 / "Identity"
|
||||
if scope_name in {"SYSTEM", "CURRENTUSER"}:
|
||||
target = target / scope_name
|
||||
elif scope_name:
|
||||
target = target / scope_name
|
||||
|
||||
try:
|
||||
target.mkdir(parents=True, exist_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return str(target)
|
||||
|
||||
|
||||
class _FileLock:
|
||||
def __init__(self, path: str) -> None:
|
||||
self.path = path
|
||||
@@ -274,9 +319,11 @@ class AgentKeyStore:
|
||||
self.scope_system = self.scope_name == "SYSTEM"
|
||||
_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._public_path = os.path.join(self.settings_dir, "agent_key.pub")
|
||||
self._guid_path = os.path.join(self.settings_dir, "guid.txt")
|
||||
self._identity_dir = _resolve_agent_identity_dir(self.settings_dir, self.scope_name)
|
||||
_ensure_dir(self._identity_dir)
|
||||
self._private_path = os.path.join(self._identity_dir, "agent_identity_private.ed25519")
|
||||
self._public_path = os.path.join(self._identity_dir, "agent_identity_public.ed25519")
|
||||
self._guid_path = os.path.join(self.settings_dir, "Agent_GUID.txt")
|
||||
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._token_meta_path = os.path.join(self.settings_dir, "access.meta.json")
|
||||
@@ -316,6 +363,7 @@ class AgentKeyStore:
|
||||
return AgentIdentity(private_key=private_key, public_key_der=public_der, public_key_b64=public_b64, fingerprint=fingerprint)
|
||||
|
||||
def _create_identity(self) -> AgentIdentity:
|
||||
_ensure_dir(os.path.dirname(self._private_path))
|
||||
private_key = ed25519.Ed25519PrivateKey.generate()
|
||||
private_bytes = private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
|
||||
Reference in New Issue
Block a user