initial commit
All checks were successful
Build k8s-mgmt-pod image / Build & Push (push) Successful in 2m21s

This commit is contained in:
Michael Trip 2026-06-23 15:06:17 +02:00
commit 4dbcde381f
20 changed files with 1151 additions and 0 deletions

82
k8s/README.md Normal file
View file

@ -0,0 +1,82 @@
# Kubernetes deployment
These manifests deploy `k8s-mgmt-pod` as a single-replica bastion deployment inside its own namespace.
## Before you apply
1. Build and publish the image.
2. Replace the placeholder image in `deployment.yaml` with a pinned tag or digest.
3. Create the real `k8s-mgmt-pod-auth` Secret instead of applying `secret.yaml.example`.
## Create the Secret
Public-key path:
```bash
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
--from-literal=ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
--from-literal=USER_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
--from-literal=ADMIN_PASSWORD='' \
--from-literal=USER_PASSWORD=''
```
Password path:
```bash
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
--from-literal=ADMIN_SSH_PUBKEY='' \
--from-literal=USER_SSH_PUBKEY='' \
--from-literal=ADMIN_PASSWORD='change-this-admin-password' \
--from-literal=USER_PASSWORD='change-this-user-password'
```
Mixed key-plus-password path:
```bash
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
--from-literal=ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
--from-literal=USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \
--from-literal=ADMIN_PASSWORD='change-this-admin-password' \
--from-literal=USER_PASSWORD='change-this-user-password'
```
With that configuration, both `admin` and `user` can authenticate with either their SSH key or their password.
Resolution order is per user:
1. If a public key is supplied, key-based login is enabled.
2. If a password is also supplied, password login is enabled too.
3. If only a public key is supplied, password login is locked for that user.
4. If neither is supplied, the container generates and logs a random password for that user.
## Apply
```bash
kubectl apply -k k8s/
```
If you prefer individual files, apply `namespace.yaml`, `serviceaccount.yaml`, `rbac.yaml`, `deployment.yaml`, `service.yaml`, and `networkpolicy.yaml` after creating the real Secret.
## Safe local access
Port-forward the service instead of exposing it externally by default:
```bash
kubectl -n k8s-mgmt-pod port-forward svc/k8s-mgmt-pod 2222:22 3000:3000
```
Then connect with:
```bash
ssh admin@localhost -p 2222
```
Or open `http://localhost:3000/ssh` in a browser.
## RBAC note
The included Role is the most security-sensitive part of this deployment. It is namespace-scoped and avoids `cluster-admin`, but it still grants broad read/write access to common namespaced resources so `lfk` remains usable as a management tool. In particular, access to `secrets`, `pods/exec`, and workload mutation verbs may be too broad for your environment and should be reduced wherever possible.
## Bastion note
This pod is a privileged operational entry point into the cluster. Treat it like a bastion: restrict who can reach it, monitor access, rotate credentials, and avoid using it as a general-purpose workstation.

92
k8s/deployment.yaml Normal file
View file

@ -0,0 +1,92 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: k8s-mgmt-pod
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: k8s-mgmt-pod
template:
metadata:
labels:
app.kubernetes.io/name: k8s-mgmt-pod
spec:
serviceAccountName: k8s-mgmt-pod
automountServiceAccountToken: true
securityContext:
seccompProfile:
type: RuntimeDefault
containers:
- name: k8s-mgmt-pod
# Replace this placeholder with a pinned tag or digest in production.
image: ghcr.io/<org>/<repo>/k8s-mgmt-pod:latest
imagePullPolicy: IfNotPresent
ports:
- name: ssh
containerPort: 22
protocol: TCP
- name: webssh
containerPort: 3000
protocol: TCP
env:
- name: ADMIN_SSH_PUBKEY
valueFrom:
secretKeyRef:
name: k8s-mgmt-pod-auth
key: ADMIN_SSH_PUBKEY
optional: true
- name: USER_SSH_PUBKEY
valueFrom:
secretKeyRef:
name: k8s-mgmt-pod-auth
key: USER_SSH_PUBKEY
optional: true
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: k8s-mgmt-pod-auth
key: ADMIN_PASSWORD
optional: true
- name: USER_PASSWORD
valueFrom:
secretKeyRef:
name: k8s-mgmt-pod-auth
key: USER_PASSWORD
optional: true
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
readinessProbe:
tcpSocket:
port: 22
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: 22
initialDelaySeconds: 15
periodSeconds: 20
volumeMounts:
- name: state
mountPath: /var/lib/k8s-mgmt-pod
securityContext:
# sshd still runs as root here because it needs to bind port 22 and switch to the authenticated user session.
# The tradeoff is deliberate: keep the container functional, then narrow capabilities instead of forcing a fragile rootless sshd setup.
runAsUser: 0
runAsGroup: 0
runAsNonRoot: false
allowPrivilegeEscalation: false
readOnlyRootFilesystem: false
capabilities:
drop: ["ALL"]
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "NET_BIND_SERVICE", "SETGID", "SETUID"]
volumes:
- name: state
emptyDir: {}

10
k8s/kustomization.yaml Normal file
View file

@ -0,0 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: k8s-mgmt-pod
resources:
- namespace.yaml
- serviceaccount.yaml
- rbac.yaml
- deployment.yaml
- service.yaml
- networkpolicy.yaml

7
k8s/namespace.yaml Normal file
View file

@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: k8s-mgmt-pod
labels:
app.kubernetes.io/name: k8s-mgmt-pod
k8s-mgmt-pod-access: "false"

24
k8s/networkpolicy.yaml Normal file
View file

@ -0,0 +1,24 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: k8s-mgmt-pod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: k8s-mgmt-pod
policyTypes:
- Ingress
ingress:
# Starting point only. Adjust these selectors to the namespaces or pods that should reach the bastion.
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: k8s-mgmt-pod
- namespaceSelector:
matchLabels:
k8s-mgmt-pod-access: "true"
ports:
- protocol: TCP
port: 22
- protocol: TCP
port: 3000

49
k8s/rbac.yaml Normal file
View file

@ -0,0 +1,49 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: k8s-mgmt-pod
rules:
# This is intentionally broader than the strict minimum so lfk and kubectl are usable.
# Scope it down to the smallest verb and resource set your operators actually need.
- apiGroups: [""]
resources:
- configmaps
- endpoints
- events
- persistentvolumeclaims
- pods
- pods/exec
- pods/log
- secrets
- serviceaccounts
- services
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["apps"]
resources:
- daemonsets
- deployments
- replicasets
- statefulsets
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["batch"]
resources:
- cronjobs
- jobs
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["networking.k8s.io"]
resources:
- ingresses
- networkpolicies
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: k8s-mgmt-pod
subjects:
- kind: ServiceAccount
name: k8s-mgmt-pod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: k8s-mgmt-pod

15
k8s/secret.yaml.example Normal file
View file

@ -0,0 +1,15 @@
# Example only. Do not commit a real Secret object with live credentials or keys.
# Create the real secret out-of-band with kubectl, Sealed Secrets, External Secrets,
# or your preferred secret manager.
apiVersion: v1
kind: Secret
metadata:
name: k8s-mgmt-pod-auth
type: Opaque
stringData:
ADMIN_SSH_PUBKEY: |
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample admin@example
USER_SSH_PUBKEY: |
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example
ADMIN_PASSWORD: "change-this-admin-password"
USER_PASSWORD: "change-this-user-password"

34
k8s/service.yaml Normal file
View file

@ -0,0 +1,34 @@
apiVersion: v1
kind: Service
metadata:
name: k8s-mgmt-pod
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: k8s-mgmt-pod
ports:
- name: ssh
port: 22
targetPort: ssh
protocol: TCP
- name: webssh
port: 3000
targetPort: webssh
protocol: TCP
# Expose externally only if you explicitly want SSH and web SSH reachable from outside the cluster.
# apiVersion: v1
# kind: Service
# metadata:
# name: k8s-mgmt-pod-external
# spec:
# type: LoadBalancer
# selector:
# app.kubernetes.io/name: k8s-mgmt-pod
# ports:
# - name: ssh
# port: 22
# targetPort: ssh
# - name: webssh
# port: 3000
# targetPort: webssh

4
k8s/serviceaccount.yaml Normal file
View file

@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: k8s-mgmt-pod