An open API service indexing awesome lists of open source software.

https://github.com/infroware/k8s-janus

Just-in-time kubectl exec access for Kubernetes. Request → Approve → Exec → Expire. No permanent permissions. Ever.
https://github.com/infroware/k8s-janus

access-control devops fastapi helm jit kubectl kubernetes pod python rbac role-based-access-control security

Last synced: 16 days ago
JSON representation

Just-in-time kubectl exec access for Kubernetes. Request → Approve → Exec → Expire. No permanent permissions. Ever.

Awesome Lists containing this project

README

          

K8s-Janus logo

# K8S-Janus

### *Just-in-Time Kubernetes Pod Access*

[![CI](https://github.com/infroware/k8s-janus/actions/workflows/ci.yaml/badge.svg)](https://github.com/infroware/k8s-janus/actions/workflows/ci.yaml)
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/k8s-janus)](https://artifacthub.io/packages/helm/k8s-janus/k8s-janus)
![Python](https://img.shields.io/badge/Python-3.12-3776AB?logo=python&logoColor=white)
![Kubernetes](https://img.shields.io/badge/Kubernetes-Operator-326CE5?logo=kubernetes&logoColor=white)
[![Helm](https://img.shields.io/badge/Helm-Chart-0F1689?logo=helm&logoColor=white)](https://infroware.github.io/k8s-janus)
![FastAPI](https://img.shields.io/badge/FastAPI-009688?logo=fastapi&logoColor=white)
![License](https://img.shields.io/badge/License-Apache_2.0-blue)

**Engineers request temporary `kubectl exec` access through a web UI.**
**Admins approve with one click. The token auto-expires.**
**No permanent permissions. Ever.**

> *In Roman mythology, **Janus** was the god of doorways — watching every passage in both directions.*
> *He did not block the gate. He governed it.*

---

## 🚨 The Problem

Most Kubernetes access patterns are broken:

| Approach | Problem |
|----------|---------|
| 🔴 Permanent RoleBinding | Over-privileged, forgotten forever |
| 🔴 Sharing cluster-admin | Dangerous, no audit trail |
| 🔴 Manual token creation | Tedious, tokens never revoked |

**K8s-Janus** replaces all of these with a structured, time-limited, fully auditable workflow.

---

## ✨ Features

| | Feature | Detail |
|--|---------|--------|
| 🌐 | **Web Terminal** | Browser-based `kubectl exec` — no local tools, no kubeconfig, no VPN |
| 🖥️ | **Split-Pane** | Two pods side-by-side in one tab with independent shell sessions |
| 📋 | **Pod Logs & Events** | Real-time logs and K8s events in the terminal sidebar, with structured diagnostics when Kubernetes calls fail |
| ⚡ | **Quick Commands** | Save and replay one-click shell commands per cluster |
| 🎨 | **Colored Prompt** | PS1 auto-injected on connect — cyan `user@host`, blue path |
| 🏢 | **Multi-Cluster** | Manage any number of clusters from one install |
| 🛰️ | **Remote Agent Mode** | On-prem/private clusters poll central Janus with local ServiceAccount auth; no stored remote kubeconfig |
| 📦 | **Multi-Namespace** | One request covers multiple namespaces; namespace tab strip in terminal |
| ✅ | **One-Click Approval** | Approve, deny, or override TTL from the admin dashboard |
| 🚪 | **Self-Service Withdraw** | Engineers cancel their own Pending or Active requests |
| ⚡ | **Instant Revoke** | Terminate any active session immediately |
| ⏰ | **Auto-Cleanup** | SA + Role + RoleBinding deleted on TTL expiry |
| ⏰ | **Pending Auto-Expiry** | Auto-deny requests that go unapproved beyond a configurable limit |
| 🔐 | **Native OIDC/SSO** | Google, GitHub, Entra ID, Okta, GitLab, or any OIDC provider — no oauth2-proxy |
| 📋 | **Full Audit Log** | Every request event, session open/close, command, and revocation logged |
| 🗄️ | **PostgreSQL Backend** | Optional persistent DB — history survives pod restarts |
| 🛡️ | **Security Hardened** | Non-root · read-only FS · all capabilities dropped · NetworkPolicy |

---

## 🔄 How It Works

```
Engineer Web UI Agent Operator Approver
│ │ │ │
│── submit ────────▶│ │ │
│ (ns=['a','b']) │── store in DB ─────▶│ │
│ │ │── notify ────────▶│
│ │ │ (clicks Approve)│
│ │ │◀── callback ──────│
│ │ │ │
│ │ ┌────────────┴────────────┐ │
│ │ │ Agent poll work item │ │
│ │ │ per-NS: SA + Role + │ │
│ │ │ RoleBinding + token │ │
│ │ └─────────────────────────┘ │
│◀── terminal ──────│ │ │
│ (namespace tabs) │ │ │
│ (TTL expires) │ Agent: delete SA + Role + RoleBinding │
```

### Access Lifecycle

```
Pending ──▶ Approved ──▶ Active ──▶ Expired
╲▶ Denied │
(any state) ──▶ Revoked
Approved ──▶ Failed (grant error — check agent logs)
```

---

## 🏗️ Architecture

```
┌─────────────────────────────────────────────────┐
│ Central Cluster │
│ │
│ ┌──────────────────┐ ┌──────────────┐ │
│ │ Web UI │ │ PostgreSQL │ │
│ │ (FastAPI+HTMX) │◀────▶│ (optional) │ │
│ └────────┬─────────┘ └──────────────┘ │
│ │ TokenReview │
└────────────┼────────────────────────────────────┘

┌────────▼────────┐ ┌────────▼────────┐
│ Remote Agent │ │ Remote Agent │
│ on Cluster A │ ... │ on Cluster B │
│ polls central │ │ polls central │
│ executes grant │ │ executes grant │
│ / cleanup work │ │ / cleanup work │
└─────────────────┘ └─────────────────┘
```

Each remote cluster runs a Janus agent that uses its own in-cluster ServiceAccount token. Central validates the agent via TokenReview — **no stored kubeconfigs, no shared secrets, no pre-registration steps.** Works with any Kubernetes cluster.

---

## 🛠️ Tech Stack

| Layer | Technology |
|-------|------------|
| **Web UI** | Python · FastAPI · HTMX · xterm.js |
| **Remote Agent** | Python · Kubernetes client library |
| **Auth** | Authlib · OIDC/OAuth2 (Google, GitHub, Entra ID, Okta, GitLab, custom) |
| **Agent Auth** | TokenReview — no shared secrets, no stored kubeconfigs |
| **Packaging** | Helm |
| **CI/CD** | GitHub Actions · Docker |

---

## 🚀 Quick Start

**Prerequisites:** `kubectl` and `helm`.

```bash
helm repo add k8s-janus https://infroware.github.io/k8s-janus
helm repo update
helm upgrade --install k8s-janus k8s-janus/k8s-janus \
--namespace k8s-janus --create-namespace
```

### Register remote clusters via agent

For any remote cluster, deploy the Janus agent:

```bash
helm upgrade --install janus-agent k8s-janus/k8s-janus \
--namespace k8s-janus --create-namespace \
--set remote.enabled=true \
--set agent.enabled=true \
--set agent.clusterName='prod-east' \
--set agent.centralUrl='https://janus.example.com'
```

The agent uses its own in-cluster ServiceAccount token to authenticate with central via TokenReview. No tokens in Git, no shared secrets, no manual registration steps.

### Configure RBAC for agent clusters

Create the `janus-pod-exec` ClusterRole on each remote cluster so agents can create per-request roles with exec access:

```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: janus-pod-exec
rules:
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "get"]
```

(This ClusterRole is created automatically when using `kubectl apply -f` from the Helm chart or when ArgoCD syncs the remote Application.)

---

## 🔐 Authentication

By default Janus trusts the `X-Forwarded-Email` header from an upstream proxy. For a self-contained setup, enable native OIDC:

```yaml
oidc:
enabled: true
provider: google # google | github | entra | okta | gitlab | custom
clientId: "your-client-id"
clientSecret: "your-client-secret"
allowedDomains: ["your-org.com"]
```

| Provider | `provider` value | Extra config |
|----------|-----------------|--------------|
| Google | `google` | — |
| GitHub | `github` | — |
| Microsoft Entra ID | `entra` | `tenantId: "your-tenant-id"` |
| Okta | `okta` | `issuerUrl: "https://your-org.okta.com"` |
| GitLab | `gitlab` | — |
| Any OIDC provider | `custom` | `issuerUrl: "https://idp.example.com"` |

`clientSecret` can be supplied inline, via `existingSecret`, or synced from a secret store via `externalSecrets.enabled: true`.

---

## 🗄️ PostgreSQL Backend

By default Janus uses SQLite (ephemeral — data lost on pod restart). For persistent history:

```yaml
postgresql:
enabled: true
host: "postgres-host"
database: "k8s-janus"
username: "k8s-janus"
```

The password must exist in a Secret named `k8s-janus-postgresql` with key `password`:

```bash
kubectl create secret generic k8s-janus-postgresql \
--namespace k8s-janus \
--from-literal=password=your-db-password
```

Or sync it automatically via External Secrets Operator:

```yaml
externalSecrets:
enabled: true
secretStore: "my-cluster-secret-store"
postgresql:
secretKey: "K8S-JANUS-DB-PASSWORD"
```

---

## ⚙️ Configuration Reference

| Field | Default | Description |
|-------|---------|-------------|
| `janus.defaultTtlSeconds` | `3600` | Default access duration in the request form (1h) |
| `janus.maxTtlSeconds` | `28800` | Hard cap engineers cannot exceed (8h) |
| `janus.approvalTtlOptions` | `[3600,7200,14400,28800]` | TTL override choices in the admin approval dropdown |
| `janus.crdRetentionSeconds` | `86400` | Delete Expired/Denied/Revoked CRDs after N seconds |
| `janus.pendingExpirySeconds` | `0` | Auto-deny Pending requests after N seconds (0 = disabled) |
| `janus.idleTimeoutSeconds` | `0` | Terminate idle terminal sessions after N seconds (0 = disabled) |
| `janus.displayTimezone` | `UTC` | IANA timezone for timestamps in the UI |
| `janus.adminEmails` | `[]` | Emails with approve/deny/revoke privileges |
| `janus.excludedNamespaces` | `[k8s-janus, kube-system, …]` | Namespaces hidden from the request form |
|| `postgresql.enabled` | `false` | Persistent DB — survives pod restarts |
| `networkPolicy.enabled` | `true` | Restrict egress to K8s API + DNS only |
| `remote.enabled` | `false` | Deploy only RBAC on a target cluster (no controller/webui) |
| `agent.enabled` | `false` | Run remote agent that polls central for work |
| `agent.clusterName` | — | Unique name for this cluster in Janus |
| `agent.centralUrl` | — | URL of the central Janus web UI (e.g. `https://janus.example.com`) |
| `tokenRefresh.enabled` | `false` | Optional validation CronJob for static-kubeconfig remote clusters; not needed for agent clusters |
| `oidc.enabled` | `false` | Enable native OIDC/OAuth2 SSO |
| `webui.authEnabled` | `false` | Trust `X-Forwarded-Email` header from upstream proxy |
| `replicaCount` | `1` | >1 also creates a PodDisruptionBudget |

---

## 🛡️ Security Model

| Control | Implementation |
|---------|---------------|
| 🔑 Token isolation | Token in K8s Secret — never in logs or visible to central |
| 🎯 Least privilege | Scoped Role per namespace, not ClusterRoleBinding |
| 🚫 No shared secrets | Agent auth via TokenReview — central validates the agent's own SA token |
| 🎯 Per-request roles | Each access request creates a unique Role with exec permission, never a shared one |
| 👤 Non-root | `runAsUser: 1000`, `runAsNonRoot: true` |
| 📁 Immutable FS | `readOnlyRootFilesystem: true` |
| 🚫 No capabilities | `capabilities.drop: [ALL]` |
| 🌐 Network isolation | NetworkPolicy: egress restricted to K8s API (443/6443) and DNS only |
| ⏰ TTL enforcement | Min 10 min · Max 8 hours · Enforced server-side |
| 🔏 Signed chart | Helm chart signed with GPG — verify with `helm install --verify` |
| 📋 Full audit trail | Every session open, close, command, idle timeout, and revocation logged |
| 🔒 Pod Security Standards | `pod-security.kubernetes.io/enforce: restricted` on the `k8s-janus` namespace |

---

## 📋 Observability

Janus logs every lifecycle event — startup, request state transitions, credential provisioning, cleanup, and WebSocket sessions.

Terminal pod/log/event API failures also emit structured diagnostics: action, cluster, namespace, pod, error code, retryability, and exception detail. The terminal UI shows the same error code and namespace context so TLS, RBAC, token-readiness, and connectivity issues are easier to debug.

```
[INFO] ✅ k8s-janus agent connected — cluster=prod-eu central=https://janus.ops.example.com
[INFO] 📥 New AccessRequest [alice-debug-api] from alice@example.com → cluster=prod-eu ns=['default','payments']
[INFO] ✅ [alice-debug-api] Approved by admin@local — TTL=3600s
[INFO] 🧹 [alice-debug-api] cleanup succeeded — all credentials removed
```

---

Apache 2.0 License · Built with ☕ by [infroware](https://github.com/infroware)