Setup Instructions
1. Install kubectl
macOS
brew install kubectl
Linux
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
2. Install kubelogin (OIDC plugin)
macOS
brew install int128/kubelogin/kubelogin
Linux
curl -LO https://github.com/int128/kubelogin/releases/latest/download/kubelogin_linux_amd64.zip
unzip kubelogin_linux_amd64.zip && sudo mv kubelogin /usr/local/bin/kubectl-oidc_login
3. Download and use your kubeconfig
# Download from the portal
curl -o ~/.kube/config-home https://k8s-portal.viktorbarzin.me/download
# Set the KUBECONFIG environment variable
export KUBECONFIG=~/.kube/config-home
# Test access (opens browser for login)
kubectl get namespaces
← Back to portal
```
Create `Dockerfile`:
```dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/build ./build
COPY --from=build /app/package.json ./
COPY --from=build /app/node_modules ./node_modules
ENV PORT=3000
EXPOSE 3000
CMD ["node", "build"]
```
Ensure SvelteKit uses the Node adapter. Update `svelte.config.js`:
```javascript
import adapter from '@sveltejs/adapter-node';
export default { kit: { adapter: adapter() } };
```
Install the Node adapter:
```bash
cd modules/kubernetes/k8s-portal/files
npm install -D @sveltejs/adapter-node
```
**Step 3: Create the Terraform module**
Create `modules/kubernetes/k8s-portal/main.tf`:
```hcl
variable "tls_secret_name" {}
variable "tier" { type = string }
resource "kubernetes_namespace" "k8s_portal" {
metadata {
name = "k8s-portal"
labels = {
tier = var.tier
}
}
}
module "tls_secret" {
source = "../setup_tls_secret"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
tls_secret_name = var.tls_secret_name
}
resource "kubernetes_deployment" "k8s_portal" {
metadata {
name = "k8s-portal"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
labels = {
app = "k8s-portal"
tier = var.tier
}
}
spec {
replicas = 1
selector {
match_labels = {
app = "k8s-portal"
}
}
template {
metadata {
labels = {
app = "k8s-portal"
}
}
spec {
container {
name = "portal"
image = "10.0.20.10:5000/k8s-portal:latest"
port {
container_port = 3000
}
volume_mount {
name = "config"
mount_path = "/config"
read_only = true
}
}
volume {
name = "config"
config_map {
name = "k8s-portal-config"
}
}
}
}
}
}
resource "kubernetes_config_map" "k8s_portal_config" {
metadata {
name = "k8s-portal-config"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
}
data = {
# CA cert extracted from kubeconfig — pass via variable or read from file
"ca.crt" = "" # Will be populated with cluster CA cert
}
}
resource "kubernetes_service" "k8s_portal" {
metadata {
name = "k8s-portal"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
}
spec {
selector = {
app = "k8s-portal"
}
port {
port = 80
target_port = 3000
}
}
}
module "ingress" {
source = "../ingress_factory"
namespace = kubernetes_namespace.k8s_portal.metadata[0].name
name = "k8s-portal"
tls_secret_name = var.tls_secret_name
protected = true # Require Authentik login
}
```
**Step 4: Add module call to `modules/kubernetes/main.tf`**
```hcl
module "k8s-portal" {
source = "./k8s-portal"
for_each = contains(local.active_modules, "authentik") ? { portal = true } : {}
tier = local.tiers.edge
tls_secret_name = var.tls_secret_name
}
```
**Step 5: Add DNS record**
Add `k8s-portal` to `cloudflare_non_proxied_names` in `terraform.tfvars`.
**Step 6: Build and push Docker image**
```bash
cd modules/kubernetes/k8s-portal/files
docker build -t 10.0.20.10:5000/k8s-portal:latest .
docker push 10.0.20.10:5000/k8s-portal:latest
```
**Step 7: Apply**
```bash
terraform apply -target=module.kubernetes_cluster.module.k8s-portal -var="kube_config_path=$(pwd)/config" -auto-approve
terraform apply -target=module.kubernetes_cluster.module.cloudflared -var="kube_config_path=$(pwd)/config" -auto-approve
```
**Step 8: Verify portal works**
Visit `https://k8s-portal.viktorbarzin.me` — should redirect to Authentik login, then show your role and kubeconfig download.
**Step 9: Commit**
```bash
git add modules/kubernetes/k8s-portal/ modules/kubernetes/main.tf
git commit -m "[ci skip] Add self-service Kubernetes access portal"
```
---
### Task 7: Create Grafana Dashboard for Audit Logs
**Files:**
- Create: `modules/kubernetes/monitoring/dashboards/k8s-audit.json`
**Step 1: Create Grafana dashboard**
Create a dashboard JSON file that queries Loki for audit logs. The dashboard should show:
- **Panel 1**: Table of recent actions (user, verb, resource, namespace, timestamp)
- **Panel 2**: Time series of request count by user
- **Panel 3**: Table of denied requests
LogQL queries:
- Recent actions: `{job="kubernetes-audit"} | json | line_format "{{.user.username}} {{.verb}} {{.objectRef.resource}} {{.objectRef.namespace}}"`
- By user: `sum by (user_username) (count_over_time({job="kubernetes-audit"} | json [5m]))`
- Denied: `{job="kubernetes-audit"} | json | responseStatus_code >= 403`
Store the dashboard JSON in `modules/kubernetes/monitoring/dashboards/k8s-audit.json` and provision it via Grafana's file provisioning (same pattern as other dashboards).
**Step 2: Apply monitoring**
```bash
terraform apply -target=module.kubernetes_cluster.module.monitoring -var="kube_config_path=$(pwd)/config" -auto-approve
```
**Step 3: Commit**
```bash
git add modules/kubernetes/monitoring/dashboards/k8s-audit.json
git commit -m "[ci skip] Add Grafana dashboard for Kubernetes audit logs"
```
---
### Task 8: End-to-End Verification
**Step 1: Test OIDC login with kubelogin**
```bash
# Install kubelogin
brew install int128/kubelogin/kubelogin
# Download kubeconfig from portal
curl -H "X-authentik-email: viktor@viktorbarzin.me" -o /tmp/test-kubeconfig https://k8s-portal.viktorbarzin.me/download
# Test kubectl with OIDC
KUBECONFIG=/tmp/test-kubeconfig kubectl get namespaces
```
This should open a browser for Authentik login, then return the namespace list.
**Step 2: Test RBAC enforcement**
Create a test namespace-owner user in `terraform.tfvars`, apply, then verify they can only access their namespace.
**Step 3: Test audit logging**
After running kubectl commands, verify they appear in Grafana:
- Go to Grafana → Explore → Loki
- Query: `{job="kubernetes-audit"} | json | user_username="viktor@viktorbarzin.me"`
**Step 4: Final commit and push**
```bash
git add -A
git commit -m "[ci skip] Multi-user Kubernetes access: complete implementation"
git push origin master
```