diff --git a/George Wilkinson FYP Proposal.pdf b/George Wilkinson FYP Proposal.pdf deleted file mode 100644 index cca2b78..0000000 Binary files a/George Wilkinson FYP Proposal.pdf and /dev/null differ diff --git a/app/apps/access/admin.py b/app/apps/access/admin.py index 890af4c..b961ed8 100644 --- a/app/apps/access/admin.py +++ b/app/apps/access/admin.py @@ -25,6 +25,8 @@ class AccessRequestAdmin(GuardedModelAdmin): "request_shell", "request_logs", "request_users", + "requested_duration_hours", + "requested_server_username", "requested_at", "expires_at", "decided_by", @@ -56,6 +58,8 @@ class AccessRequestAdmin(GuardedModelAdmin): "request_shell", "request_logs", "request_users", + "requested_duration_hours", + "requested_server_username", "expires_at", ) }, @@ -73,6 +77,8 @@ class AccessRequestAdmin(GuardedModelAdmin): "request_shell", "request_logs", "request_users", + "requested_duration_hours", + "requested_server_username", "expires_at", ) }, diff --git a/app/apps/access/migrations/0004_access_request_scope_metadata.py b/app/apps/access/migrations/0004_access_request_scope_metadata.py new file mode 100644 index 0000000..8ca38a2 --- /dev/null +++ b/app/apps/access/migrations/0004_access_request_scope_metadata.py @@ -0,0 +1,21 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("access", "0003_access_request_options"), + ] + + operations = [ + migrations.AddField( + model_name="accessrequest", + name="requested_duration_hours", + field=models.PositiveIntegerField(blank=True, null=True), + ), + migrations.AddField( + model_name="accessrequest", + name="requested_server_username", + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/app/apps/access/models.py b/app/apps/access/models.py index 9b13d75..eeed5f4 100644 --- a/app/apps/access/models.py +++ b/app/apps/access/models.py @@ -31,6 +31,8 @@ class AccessRequest(models.Model): request_shell = models.BooleanField(default=False) request_logs = models.BooleanField(default=False) request_users = models.BooleanField(default=False) + requested_duration_hours = models.PositiveIntegerField(null=True, blank=True) + requested_server_username = models.CharField(max_length=128, blank=True) requested_at = models.DateTimeField(default=timezone.now, editable=False) decided_at = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) diff --git a/app/apps/accounts/templates/accounts/profile.html b/app/apps/accounts/templates/accounts/profile.html index 1b07ad1..4de992f 100644 --- a/app/apps/accounts/templates/accounts/profile.html +++ b/app/apps/accounts/templates/accounts/profile.html @@ -458,11 +458,9 @@ {% endblock %} diff --git a/app/apps/servers/templates/servers/dashboard.html b/app/apps/servers/templates/servers/dashboard.html index 3660f03..08d67c4 100644 --- a/app/apps/servers/templates/servers/dashboard.html +++ b/app/apps/servers/templates/servers/dashboard.html @@ -138,14 +138,18 @@ Request pending since {{ item.access.pending.requested_at|date:"M j, Y H:i" }} {% else %} -
- {% csrf_token %} - -
+ {% endif %} -

Request includes shell, logs, and users scopes.

+

Choose scopes, duration, and preferred server username in the request popup.

{% endif %} @@ -164,4 +168,144 @@ {% endif %} + + + + {% endblock %} diff --git a/app/apps/servers/templates/servers/detail.html b/app/apps/servers/templates/servers/detail.html index 137f984..3a2f8c5 100644 --- a/app/apps/servers/templates/servers/detail.html +++ b/app/apps/servers/templates/servers/detail.html @@ -129,11 +129,9 @@ - + diff --git a/app/theme/templates/base.html b/app/theme/templates/base.html index 0c40aeb..e5cb2fd 100644 --- a/app/theme/templates/base.html +++ b/app/theme/templates/base.html @@ -1,4 +1,4 @@ -{% load static tailwind_tags %} +{% load static %} @@ -6,7 +6,7 @@ - {% tailwind_css %} + diff --git a/evidence/diagrams/dissertation/01-system-architecture.mmd b/evidence/diagrams/dissertation/01-system-architecture.mmd new file mode 100644 index 0000000..04e91b0 --- /dev/null +++ b/evidence/diagrams/dissertation/01-system-architecture.mmd @@ -0,0 +1,33 @@ +flowchart LR + U[User Browser] + A[Admin Browser] + I[OIDC IdP] + + subgraph K[Keywarden Control Plane] + UI[Web UI + API] + WS[WebSocket Shell Service] + DB[(PostgreSQL)] + R[(Redis/Valkey)] + UCA[(User SSH CA)] + ACA[(Agent mTLS CA)] + end + + subgraph M[Managed Infrastructure] + AG[Keywarden Agent] + H[Linux Host + sshd] + end + + U --> UI + A --> UI + UI <--> I + UI --> DB + UI --> R + UI --> UCA + UI --> ACA + + AG <--> UI + AG --> H + U --> H + + U -.optional browser shell.-> WS + WS --> H diff --git a/evidence/diagrams/dissertation/02-deployment-topology.mmd b/evidence/diagrams/dissertation/02-deployment-topology.mmd new file mode 100644 index 0000000..11c4966 --- /dev/null +++ b/evidence/diagrams/dissertation/02-deployment-topology.mmd @@ -0,0 +1,37 @@ +flowchart TB + subgraph Z0[Public Network] + B[Browsers] + AG[Agents] + end + + subgraph Z1[Edge] + T[Traefik] + end + + subgraph Z2[Keywarden Host] + N[Nginx] + G[Gunicorn Django] + D[Daphne Channels] + C[Celery Worker + Beat] + P[(PostgreSQL)] + V[(Redis/Valkey)] + K[(CA Key Files)] + end + + subgraph Z3[Managed Hosts] + SSH[sshd + local accounts] + end + + B -->|HTTPS 443| T + AG -->|HTTPS 443| T + T -->|HTTPS 443| N + N -->|HTTP 8000| G + N -->|WS 8001| D + G --> P + G --> V + C --> P + C --> V + G --> K + AG --> SSH + + AG -.mTLS cert presented on /api/v1/agent/.-> N diff --git a/evidence/diagrams/dissertation/03-data-model-erd.mmd b/evidence/diagrams/dissertation/03-data-model-erd.mmd new file mode 100644 index 0000000..0088228 --- /dev/null +++ b/evidence/diagrams/dissertation/03-data-model-erd.mmd @@ -0,0 +1,43 @@ +erDiagram + USER ||--o{ SSHKEY : owns + SSHKEY ||--o| SSHCERTIFICATE : has_active_cert + + USER ||--o{ ACCESSREQUEST : submits + SERVER ||--o{ ACCESSREQUEST : targets + + SERVER ||--o{ SERVERACCOUNT : tracks + USER ||--o{ SERVERACCOUNT : maps_to_system_user + + SERVER ||--o{ SERVERAUDITLOG : receives + SERVER ||--o{ SERVERLOGSOURCE : configures + SERVER ||--o{ TELEMETRYEVENT : emits + USER ||--o{ AUDITLOG : actor + + SSHCERTIFICATEAUTHORITY ||--o{ SSHCERTIFICATE : signs + AGENTCERTIFICATEAUTHORITY ||--o{ SERVER : enrolls + SERVER ||--o{ ENROLLMENTTOKEN : bootstraps + + USER { + int id PK + string email + bool is_active + } + SERVER { + int id PK + string display_name + string hostname + datetime last_heartbeat_at + } + ACCESSREQUEST { + int id PK + string status + bool request_shell + bool request_logs + bool request_users + datetime expires_at + } + SSHKEY { + int id PK + string fingerprint + bool is_active + } diff --git a/evidence/diagrams/dissertation/04-access-governance-sequence.mmd b/evidence/diagrams/dissertation/04-access-governance-sequence.mmd new file mode 100644 index 0000000..a2359a8 --- /dev/null +++ b/evidence/diagrams/dissertation/04-access-governance-sequence.mmd @@ -0,0 +1,27 @@ +sequenceDiagram + participant U as User + participant K as Keywarden + participant AD as Administrator + participant AG as Agent + participant H as Managed Host + + U->>K: Submit access request (shell/logs/users) + K-->>U: Request status = pending + AD->>K: Approve request + K-->>AD: Grant active + + loop Poll every N seconds + AG->>K: Fetch accounts + CA + log policy + K-->>AG: Effective access state + end + + AG->>H: Create/lock accounts + apply trust config + AG->>K: Sync report + heartbeat + logs + K-->>AD: Updated admin/audit view + + AD->>K: Revoke or let request expire + loop Next poll + AG->>K: Pull updated state + K-->>AG: User removed/expired + end + AG->>H: Enforce revocation diff --git a/evidence/diagrams/dissertation/05-agent-enrollment-runtime-auth-sequence.mmd b/evidence/diagrams/dissertation/05-agent-enrollment-runtime-auth-sequence.mmd new file mode 100644 index 0000000..ff279f6 --- /dev/null +++ b/evidence/diagrams/dissertation/05-agent-enrollment-runtime-auth-sequence.mmd @@ -0,0 +1,27 @@ +sequenceDiagram + participant AD as Administrator + participant AG as Agent + participant K as Keywarden API + participant N as Nginx + + AD->>K: Create one-time enrollment token + AG->>AG: Generate keypair + CSR + AG->>K: POST /agent/enroll (token + CSR + host/IP) + K->>K: Validate token, create/update Server, sign client cert + K-->>AG: server_id + client_cert + ca_cert (+ token metadata) + AG->>AG: Persist cert/key/ca and server_id + + loop Runtime polling + AG->>N: /api/v1/agent/* with client cert + N->>K: Forward request + TLS client headers + K->>K: Validate mTLS cert fingerprint/serial against server + K-->>AG: 200 + policy/state payload + end + + alt mTLS context missing and fallback enabled + AG->>K: Authorization: Bearer + K->>K: Validate server/global token + K-->>AG: 200 (compatibility mode) + else mTLS strict mode + K-->>AG: 401 Unauthorized + end diff --git a/evidence/diagrams/dissertation/06-revocation-latency-timeline.mmd b/evidence/diagrams/dissertation/06-revocation-latency-timeline.mmd new file mode 100644 index 0000000..5d071b2 --- /dev/null +++ b/evidence/diagrams/dissertation/06-revocation-latency-timeline.mmd @@ -0,0 +1,19 @@ +sequenceDiagram + participant AD as Administrator + participant K as Keywarden + participant AG as Agent + participant H as Managed Host + participant U as User + + AD->>K: Revoke access at t0 + Note right of K: Policy state changes immediately + + loop Poll interval = N seconds + AG->>K: Poll /accounts + /ssh-ca + end + + K-->>AG: Revoked state at t0 + delta + AG->>H: Remove/lock account and apply new state + H-->>U: SSH denied + + Note over AD,U: Effective revocation latency <= poll interval + agent apply time diff --git a/evidence/diagrams/dissertation/07-browser-shell-trust-flow.mmd b/evidence/diagrams/dissertation/07-browser-shell-trust-flow.mmd new file mode 100644 index 0000000..90266ed --- /dev/null +++ b/evidence/diagrams/dissertation/07-browser-shell-trust-flow.mmd @@ -0,0 +1,23 @@ +sequenceDiagram + participant U as Browser User + participant UI as Shell UI + participant SC as ShellConsumer + participant CA as User SSH CA + participant H as Target sshd + + U->>UI: Open /servers/{id}/shell + UI->>SC: Open WebSocket /ws/servers/{id}/shell + SC->>SC: Authenticate session + object permission check + + SC->>SC: Generate ephemeral keypair (tmp dir) + SC->>CA: Sign ephemeral public key (short TTL) + CA-->>SC: SSH certificate + + SC->>H: Start ssh -tt with key + cert + U<->>SC: Terminal I/O over WebSocket + SC<->>H: SSH stream + + U->>SC: Disconnect/close tab + SC->>SC: Terminate process and clean temp key material + + Note over U,H: Feature is beta; session transcript retention is limited diff --git a/evidence/diagrams/dissertation/08-audit-pipeline-flow.mmd b/evidence/diagrams/dissertation/08-audit-pipeline-flow.mmd new file mode 100644 index 0000000..46f26fc --- /dev/null +++ b/evidence/diagrams/dissertation/08-audit-pipeline-flow.mmd @@ -0,0 +1,21 @@ +flowchart LR + subgraph S[Event Sources] + API[API Requests] + WS[WebSocket Shell Events] + AL[Agent Log Batches] + HB[Agent Heartbeat/Sync Reports] + end + + API --> MM[Audit Matcher + Event Type Rules] + WS --> MM + MM --> AL1[(AuditLog)] + + AL --> N1[Normalize + Parse] + N1 --> AL2[(ServerAuditLog)] + + HB --> T1[(TelemetryEvent)] + + AL1 --> UI[Admin Audit Views] + AL2 --> UI + T1 --> UI + UI --> EV[Evidence Export: logs, CSV, summaries] diff --git a/evidence/diagrams/dissertation/09-access-request-state-machine.mmd b/evidence/diagrams/dissertation/09-access-request-state-machine.mmd new file mode 100644 index 0000000..5a63980 --- /dev/null +++ b/evidence/diagrams/dissertation/09-access-request-state-machine.mmd @@ -0,0 +1,14 @@ +stateDiagram-v2 + [*] --> Pending: request created + + Pending --> Approved: admin approve + Pending --> Denied: admin deny + Pending --> Cancelled: requester cancel + + Approved --> Revoked: admin revoke + Approved --> Expired: expires_at reached + + Denied --> [*] + Cancelled --> [*] + Revoked --> [*] + Expired --> [*] diff --git a/evidence/diagrams/dissertation/10-threat-trust-boundaries.mmd b/evidence/diagrams/dissertation/10-threat-trust-boundaries.mmd new file mode 100644 index 0000000..8258917 --- /dev/null +++ b/evidence/diagrams/dissertation/10-threat-trust-boundaries.mmd @@ -0,0 +1,38 @@ +flowchart LR + ATT[External Attacker] + + subgraph Z0[Untrusted Network] + U[User/Admin Browsers] + AG[Agents] + end + + subgraph Z1[Edge Trust Boundary] + T[Traefik] + end + + subgraph Z2[Keywarden Trusted Zone] + N[Nginx] + APP[Keywarden App] + DB[(PostgreSQL)] + R[(Redis/Valkey)] + CA[(CA Private Keys)] + end + + subgraph Z3[Managed Host Zone] + SSH[sshd + local accounts] + end + + U -->|HTTPS| T -->|HTTPS| N --> APP + AG -->|HTTPS/mTLS path| T + AG --> SSH + APP --> DB + APP --> R + APP --> CA + + ATT -.credential theft / token misuse.-> T + ATT -.injection / auth bypass attempts.-> APP + ATT -.MITM if proxy TLS/mTLS misconfigured.-> N + ATT -.host compromise impact.-> SSH + + Note1[Key Risk: CA private keys are central high-value assets] + CA --- Note1 diff --git a/evidence/diagrams/dissertation/README.md b/evidence/diagrams/dissertation/README.md new file mode 100644 index 0000000..f4720f4 --- /dev/null +++ b/evidence/diagrams/dissertation/README.md @@ -0,0 +1,22 @@ +# Dissertation Diagram Set + +This folder contains compact Mermaid diagrams for Chapters 4-7. + +## Files + +- `01-system-architecture.mmd` +- `02-deployment-topology.mmd` +- `03-data-model-erd.mmd` +- `04-access-governance-sequence.mmd` +- `05-agent-enrollment-runtime-auth-sequence.mmd` +- `06-revocation-latency-timeline.mmd` +- `07-browser-shell-trust-flow.mmd` +- `08-audit-pipeline-flow.mmd` +- `09-access-request-state-machine.mmd` +- `10-threat-trust-boundaries.mmd` + +## Render Example + +```bash +mmdc -i 01-system-architecture.mmd -o 01-system-architecture.png +``` diff --git a/nginx/configs/options-http-headers.conf b/nginx/configs/options-http-headers.conf index a53dd1a..b214a18 100644 --- a/nginx/configs/options-http-headers.conf +++ b/nginx/configs/options-http-headers.conf @@ -1,5 +1,8 @@ -add_header Content-Security-Policy "default-src 'self'; font-src *;img-src * data:; script-src * 'unsafe-eval' 'unsafe-inline'; style-src * 'unsafe-inline'"; -add_header X-Frame-Options "SAMEORIGIN"; -add_header X-Content-Type-Options nosniff; -add_header Referrer-Policy "strict-origin"; -add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()"; +proxy_hide_header X-Powered-By; + +add_header Content-Security-Policy "default-src 'self'; base-uri 'self'; frame-ancestors 'self'; object-src 'none'; form-action 'self'; connect-src 'self' https: wss:; img-src 'self' data: https:; font-src 'self' data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "geolocation=(),midi=(),sync-xhr=(),microphone=(),camera=(),magnetometer=(),gyroscope=(),fullscreen=(self),payment=()" always; +add_header X-Permitted-Cross-Domain-Policies "none" always; diff --git a/nginx/configs/options-https-headers.conf b/nginx/configs/options-https-headers.conf index d2a4972..a7a62ca 100644 --- a/nginx/configs/options-https-headers.conf +++ b/nginx/configs/options-https-headers.conf @@ -1,2 +1,2 @@ include options-http-headers.conf; -add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains; preload'; +add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;