From b95084ddc35099f4931a21f55264eff20dd169a9 Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 25 Jan 2026 23:08:40 +0000 Subject: [PATCH] Linux agent functional. Added new client-facing server panel. Removed deferred pydantic annotations. --- agent/config.example.json | 6 +- agent/keywarden-agent | Bin 9011088 -> 9011088 bytes .../accounts/templates/accounts/login.html | 3 +- app/apps/servers/admin.py | 3 +- .../servers/templates/servers/dashboard.html | 67 +++++++++++++++ .../servers/templates/servers/detail.html | 78 ++++++++++++++++++ app/apps/servers/urls.py | 10 +++ app/apps/servers/views.py | 67 +++++++++++++++ app/keywarden/api/routers/agent.py | 40 +++++---- app/keywarden/settings/base.py | 2 +- app/keywarden/urls.py | 3 +- app/templates/base.html | 2 +- 12 files changed, 253 insertions(+), 28 deletions(-) create mode 100644 app/apps/servers/templates/servers/dashboard.html create mode 100644 app/apps/servers/templates/servers/detail.html create mode 100644 app/apps/servers/urls.py create mode 100644 app/apps/servers/views.py diff --git a/agent/config.example.json b/agent/config.example.json index f0ebbc8..3b25335 100644 --- a/agent/config.example.json +++ b/agent/config.example.json @@ -1,6 +1,6 @@ { - "server_url": "https://keywarden.example.com", - "server_id": "", + "server_url": "https://keywarden.dev.ntbx.io/api/v1", + "server_id": "4", "sync_interval_seconds": 30, "log_batch_size": 500, "state_dir": "/var/lib/keywarden-agent", @@ -11,4 +11,4 @@ "create_home": true, "lock_on_revoke": true } -} +} \ No newline at end of file diff --git a/agent/keywarden-agent b/agent/keywarden-agent index db4bcb758b063c660c8fd5aabd33e1aa63a00afe..65d6dc5db25e9816ee8821edc4264a44bbcb5002 100755 GIT binary patch delta 867 zcmb`?&r=d{9LDhtu(UEN`(fGdpPJz=UzS~_;mO;2_#?2#0Y5M{x|taRMiC3a4=f zXVH!hoI@wh!-X!$P~e7u2P(YKpu-100=R&SxK!62!G@+5A%rRvm+JF*H6gOvn4#6< z&B3It{oj*E7^c^)Y8lHkm8_Ok{F+}=Ouj9B^@lUq)+j_kr~#qW)}29g<1%{Ciz~Q_ zYv@A=VO&Q)B8Vb}0mPBOAZ{Run;1e0w{RP24A$ijq$apW+8JPMe^6pAQe8fDDj0cPIB3?aE5Jn2DXaEODL`}u8hm{#^I>()# zpwpsFJ36k~nc8fu`G3eO|10)oR`wSf3*Cn zEcEnkY<#i`f{?^kMy3S8GdrV)TqCBIT};hIN0U;d7#9P6mzvh}VkS6QD+S77`*eLm zlZbe*Htqs-Mf>v0t z)!VQQ+tH2=?7&X!LML{^hCSGeedxk|9Kb;wLN^Yh2fa9gqv*pi9LEWq#3`J{8JtBw z25=7NF^CJW<03>zkl}z63S4kQg$572xP;5N(p0U!wvH}YRWw<#yWOT~I15fQ=WsP+ zoxWu2z`v6rsfuYBMov=QPT8qxE{B@W=kki=G1k7@e7)_m>QywatTZ-lK3v5$_;DQp z1Q9|Q5!}E{+`?@{F@#~n5XT4-NJ7UbQn-V=NF&qSw2gT`{`V_uTp4Uw*2i!U_wfK( zJcNN9@+e>&CW^3e%XuW6WX>^Qd6~Pf*7qp5hsv;{}%R60h(Y%Xoto NyhWpFS%3Hb+g|`w8QcH> diff --git a/app/apps/accounts/templates/accounts/login.html b/app/apps/accounts/templates/accounts/login.html index cb4e755..3310642 100644 --- a/app/apps/accounts/templates/accounts/login.html +++ b/app/apps/accounts/templates/accounts/login.html @@ -8,7 +8,7 @@

Sign in

{% csrf_token %} - +
@@ -35,4 +35,3 @@
{% endblock %} - diff --git a/app/apps/servers/admin.py b/app/apps/servers/admin.py index 839c387..2fb9b51 100644 --- a/app/apps/servers/admin.py +++ b/app/apps/servers/admin.py @@ -59,12 +59,11 @@ class AgentCertificateAuthorityAdmin(admin.ModelAdmin): list_display = ("name", "is_active", "created_at", "revoked_at") list_filter = ("is_active", "created_at", "revoked_at") search_fields = ("name", "fingerprint") - readonly_fields = ("fingerprint", "serial", "created_at", "revoked_at", "created_by") + readonly_fields = ("cert_pem", "fingerprint", "serial", "created_at", "revoked_at", "created_by") fields = ( "name", "is_active", "cert_pem", - "key_pem", "fingerprint", "serial", "created_by", diff --git a/app/apps/servers/templates/servers/dashboard.html b/app/apps/servers/templates/servers/dashboard.html new file mode 100644 index 0000000..28b7bb7 --- /dev/null +++ b/app/apps/servers/templates/servers/dashboard.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}Servers • Keywarden{% endblock %} + +{% block content %} +
+
+

Servers

+

Your active server access and recent activity at a glance.

+
+ + {% if servers %} +
+ {% for item in servers %} +
+
+
+
+ {{ item.server.initial }} +
+
+

{{ item.server.display_name }}

+

+ {{ item.server.hostname|default:item.server.ipv4|default:item.server.ipv6|default:"Unassigned" }} +

+
+
+ Active +
+ +
+
+
Access until
+
+ {% if item.expires_at %} + {{ item.expires_at|date:"M j, Y H:i" }} + {% else %} + No expiry + {% endif %} +
+
+
+
Last accessed
+
+ {% if item.last_accessed %} + {{ item.last_accessed|date:"M j, Y H:i" }} + {% else %} + — + {% endif %} +
+
+
+ + +
+ {% endfor %} +
+ {% else %} +
+

No server access yet

+

Request access to a server to see it here.

+
+ {% endif %} +
+{% endblock %} diff --git a/app/apps/servers/templates/servers/detail.html b/app/apps/servers/templates/servers/detail.html new file mode 100644 index 0000000..efa2184 --- /dev/null +++ b/app/apps/servers/templates/servers/detail.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}{{ server.display_name }} • Keywarden{% endblock %} + +{% block content %} +
+
+
+
+ {{ server.initial }} +
+
+

{{ server.display_name }}

+

+ {{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }} +

+
+
+ Back to servers +
+ +
+
+

Access

+
+
+
Access until
+
+ {% if expires_at %} + {{ expires_at|date:"M j, Y H:i" }} + {% else %} + No expiry + {% endif %} +
+
+
+
Last accessed
+
+ {% if last_accessed %} + {{ last_accessed|date:"M j, Y H:i" }} + {% else %} + — + {% endif %} +
+
+
+
+ +
+

Server details

+
+
+
Hostname
+
{{ server.hostname|default:"—" }}
+
+
+
IPv4
+
{{ server.ipv4|default:"—" }}
+
+
+
IPv6
+
{{ server.ipv6|default:"—" }}
+
+
+
+
+ +
+
+

Logs

+ Placeholder +
+
+ Logs will appear here once collection is enabled for this server. +
+
+
+{% endblock %} diff --git a/app/apps/servers/urls.py b/app/apps/servers/urls.py new file mode 100644 index 0000000..4e1ce0d --- /dev/null +++ b/app/apps/servers/urls.py @@ -0,0 +1,10 @@ +from django.urls import path + +from . import views + +app_name = "servers" + +urlpatterns = [ + path("", views.dashboard, name="dashboard"), + path("/", views.detail, name="detail"), +] diff --git a/app/apps/servers/views.py b/app/apps/servers/views.py new file mode 100644 index 0000000..f532e61 --- /dev/null +++ b/app/apps/servers/views.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from django.contrib.auth.decorators import login_required +from django.db.models import Q +from django.http import Http404 +from django.shortcuts import render +from django.utils import timezone + +from apps.access.models import AccessRequest + + +@login_required(login_url="/accounts/login/") +def dashboard(request): + now = timezone.now() + access_qs = ( + AccessRequest.objects.select_related("server") + .filter( + requester=request.user, + status=AccessRequest.Status.APPROVED, + ) + .filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now)) + .order_by("-requested_at") + ) + + seen = set() + servers = [] + for access in access_qs: + if access.server_id in seen: + continue + seen.add(access.server_id) + servers.append( + { + "server": access.server, + "expires_at": access.expires_at, + "last_accessed": None, + } + ) + + context = { + "servers": servers, + } + return render(request, "servers/dashboard.html", context) + + +@login_required(login_url="/accounts/login/") +def detail(request, server_id: int): + now = timezone.now() + access = ( + AccessRequest.objects.select_related("server") + .filter( + requester=request.user, + server_id=server_id, + status=AccessRequest.Status.APPROVED, + ) + .filter(Q(expires_at__isnull=True) | Q(expires_at__gt=now)) + .order_by("-requested_at") + .first() + ) + if not access: + raise Http404("Server not found") + + context = { + "server": access.server, + "expires_at": access.expires_at, + "last_accessed": None, + } + return render(request, "servers/detail.html", context) diff --git a/app/keywarden/api/routers/agent.py b/app/keywarden/api/routers/agent.py index 7ba0115..74dc6bd 100644 --- a/app/keywarden/api/routers/agent.py +++ b/app/keywarden/api/routers/agent.py @@ -1,5 +1,3 @@ -from __future__ import annotations - from datetime import datetime, timedelta from typing import List, Optional @@ -8,10 +6,11 @@ from cryptography.hazmat.primitives import hashes, serialization from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID from django.conf import settings from django.core.exceptions import ValidationError -from django.db import models +from django.db import IntegrityError, models, transaction from django.http import HttpRequest from django.utils import timezone -from ninja import Router, Schema +from django.views.decorators.csrf import csrf_exempt +from ninja import Body, Router, Schema from ninja.errors import HttpError from pydantic import Field @@ -78,7 +77,8 @@ def build_router() -> Router: router = Router() @router.post("/enroll", response=AgentEnrollOut, auth=None) - def enroll_agent(request: HttpRequest, payload: AgentEnrollIn): + @csrf_exempt + def enroll_agent(request: HttpRequest, payload: AgentEnrollIn = Body(...)): """Enroll a server agent using a one-time token.""" token_value = (payload.token or "").strip() if not token_value: @@ -100,16 +100,19 @@ def build_router() -> Router: except ValidationError: hostname = None - server = Server.objects.create(display_name=display_name, hostname=hostname) - token.mark_used(server) - token.save(update_fields=["used_at", "server"]) - csr = _load_csr((payload.csr_pem or "").strip()) - cert_pem, ca_pem, fingerprint, serial = _issue_client_cert(csr, host, server.id) - server.agent_enrolled_at = timezone.now() - server.agent_cert_fingerprint = fingerprint - server.agent_cert_serial = serial - server.save(update_fields=["agent_enrolled_at", "agent_cert_fingerprint", "agent_cert_serial"]) + try: + with transaction.atomic(): + server = Server.objects.create(display_name=display_name, hostname=hostname) + token.mark_used(server) + token.save(update_fields=["used_at", "server"]) + cert_pem, ca_pem, fingerprint, serial = _issue_client_cert(csr, host, server.id) + server.agent_enrolled_at = timezone.now() + server.agent_cert_fingerprint = fingerprint + server.agent_cert_serial = serial + server.save(update_fields=["agent_enrolled_at", "agent_cert_fingerprint", "agent_cert_serial"]) + except IntegrityError: + raise HttpError(409, "Server already enrolled") return AgentEnrollOut( server_id=str(server.id), @@ -153,10 +156,10 @@ def build_router() -> Router: for key in keys ] - @router.post("/servers/{server_id}/sync-report", response=SyncReportOut) - def sync_report(request: HttpRequest, server_id: int, payload: SyncReportIn): + @router.post("/servers/{server_id}/sync-report", response=SyncReportOut, auth=None) + @csrf_exempt + def sync_report(request: HttpRequest, server_id: int, payload: SyncReportIn = Body(...)): """Record an agent sync report for a server (admin or operator).""" - require_perms(request, "servers.view_server", "telemetry.add_telemetryevent") try: server = Server.objects.get(id=server_id) except Server.DoesNotExist: @@ -176,7 +179,8 @@ def build_router() -> Router: return SyncReportOut(status="ok") @router.post("/servers/{server_id}/logs", response=LogIngestOut, auth=None) - def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn]): + @csrf_exempt + def ingest_logs(request: HttpRequest, server_id: int, payload: List[LogEventIn] = Body(...)): """Accept log batches from agents (mTLS required at the edge).""" try: Server.objects.get(id=server_id) diff --git a/app/keywarden/settings/base.py b/app/keywarden/settings/base.py index c0524c2..40f254c 100644 --- a/app/keywarden/settings/base.py +++ b/app/keywarden/settings/base.py @@ -226,7 +226,7 @@ else: ] LOGIN_URL = "/accounts/login/" LOGOUT_URL = "/oidc/logout/" -LOGIN_REDIRECT_URL = "/" +LOGIN_REDIRECT_URL = "/servers/" LOGOUT_REDIRECT_URL = "/" ANONYMOUS_USER_NAME = None diff --git a/app/keywarden/urls.py b/app/keywarden/urls.py index 1fcc001..dd71801 100644 --- a/app/keywarden/urls.py +++ b/app/keywarden/urls.py @@ -8,10 +8,11 @@ urlpatterns = [ path("admin/", admin.site.urls), path("oidc/", include("mozilla_django_oidc.urls")), path("accounts/", include("apps.accounts.urls")), + path("servers/", include("apps.servers.urls")), # API path("api/", ninja_api.urls), path("api/v1/", ninja_api_v1.urls), path("api/auth/jwt/create/", TokenObtainPairView.as_view(), name="jwt-create"), path("api/auth/jwt/refresh/", TokenRefreshView.as_view(), name="jwt-refresh"), - path("", RedirectView.as_view(pattern_name="accounts:login", permanent=False)), + path("", RedirectView.as_view(pattern_name="servers:dashboard", permanent=False)), ] diff --git a/app/templates/base.html b/app/templates/base.html index ab55513..4fefd9a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -19,6 +19,7 @@