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

212
README.md Normal file
View file

@ -0,0 +1,212 @@
# k8s-mgmt-pod
`k8s-mgmt-pod` is a Debian-based Kubernetes bastion image with:
- `lfk` preinstalled from a pinned GitHub release `.deb`
- `kubectl` preinstalled from a pinned upstream binary release
- `helm` preinstalled from a pinned upstream binary release
- `k9s` preinstalled from a pinned GitHub release tarball
- `kubectl` and `helm` bash autocompletion enabled for both `admin` and `user`
- local `openssh-server` on port `22`
- a browser-based SSH client on port `3000`
- a custom ASCII-art MOTD on login
- two provisioned users: `admin` and `user`
## Why WebSSH2
This image uses [WebSSH2](https://github.com/billchurch/webssh2) for browser access instead of a generic web terminal wrapper. WebSSH2 is an actively maintained SSH client that speaks SSH to the local `sshd` in the same container, which matches the bastion use case better than wrapping a shell command directly.
One caveat is worth stating clearly: browser-based SSH tools do not keep the private-key handshake entirely in the browser. If a user chooses key-based login in the WebSSH2 UI, the key material is passed to the server-side SSH client for the actual SSH authentication flow.
## Ports
- `22`: native SSH into the container's local `sshd`
- `3000`: browser UI entrypoint; `/` and `/ssh` are forced to a fixed local WebSSH2 session for `127.0.0.1` and rewritten back to that host if changed in the URL
## Runtime environment variables
The entrypoint configures authentication on every container start. A user can have both SSH public keys and a password at the same time.
| Variable | Meaning |
|---|---|
| `ADMIN_SSH_PUBKEY` | One or more newline-separated SSH public keys for `admin` |
| `USER_SSH_PUBKEY` | One or more newline-separated SSH public keys for `user` |
| `ADMIN_PASSWORD` | Optional password for `admin`; can be used with or without `ADMIN_SSH_PUBKEY` |
| `USER_PASSWORD` | Optional password for `user`; can be used with or without `USER_SSH_PUBKEY` |
| `ENABLE_BOOTSTRAP_KUBECONFIG` | Set to `true` to copy a mounted kubeconfig into both users' homes at startup; defaults to `false` |
| `BOOTSTRAP_KUBECONFIG` | Path to the mounted kubeconfig source file when bootstrap is enabled; defaults to `/bootstrap/kubeconfig/config` |
Behavior per user:
1. If `*_SSH_PUBKEY` is non-empty, the entrypoint writes `authorized_keys`, fixes ownership and permissions, and enables SSH key login for that user.
2. If `*_PASSWORD` is set, the entrypoint also sets that password, even when a public key is present.
3. If no password is supplied but a public key is present, the entrypoint locks password login for that user and leaves key-based login enabled.
4. If neither a public key nor a password is supplied, the entrypoint generates a random password, stores it in the container state directory, applies it, and logs it once to container stdout with a clear label.
If both users end up key-only, `sshd` is configured for public-key-only login. If either user has an explicit or generated password, `sshd` enables password and keyboard-interactive authentication so both native SSH and WebSSH2 can use the same underlying auth methods.
## Build
```bash
docker build --load -t k8s-mgmt-pod:local .
```
If your Docker CLI is backed by Buildx with the `docker-container` driver, a plain `docker build` may finish successfully but leave the image only in the build cache. `--load` imports the result into your local Docker image store so `docker run k8s-mgmt-pod:local` works afterward.
Pinned build arguments can be overridden when you need to bump releases:
```bash
docker build --load \
--build-arg LFK_VERSION=0.14.9 \
--build-arg KUBECTL_VERSION=1.31.0 \
--build-arg HELM_VERSION=3.16.2 \
--build-arg K9S_VERSION=0.40.10 \
--build-arg WEBSSH2_VERSION=5.0.1 \
-t k8s-mgmt-pod:local .
```
## Run with Docker Compose
The included [docker-compose.yaml](docker-compose.yaml) enables kubeconfig bootstrap for local use. It mounts your host `${HOME}/.kube` directory read-only and copies `${HOME}/.kube/config` into both `/home/admin/.kube/config` and `/home/user/.kube/config` at container startup with the correct ownership and `0600` permissions.
That bootstrap is explicitly opt-in and defaults to off in the container itself, so it does not run in Kubernetes unless you deliberately enable it.
Start it with:
```bash
docker compose up --build
```
Or detached:
```bash
docker compose up -d --build
docker compose logs -f
```
If `2222` or `3000` is already in use on your host, override the published ports:
```bash
HOST_SSH_PORT=32222 HOST_WEBSSH_PORT=33000 docker compose up -d --build
```
You can override the default passwords or inject SSH public keys through your shell environment before starting Compose:
```bash
export ADMIN_PASSWORD='change-this-admin-password'
export USER_PASSWORD='change-this-user-password'
export ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)"
export USER_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)"
docker compose up -d --build
```
You can also start from the example environment file:
```bash
cp .env.example .env
docker compose up -d --build
```
If `${HOME}/.kube/config` is missing on the host, the container logs that bootstrap was skipped and continues normally.
## Run with SSH public keys
```bash
docker run --rm \
-p 2222:22 \
-p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
-e USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \
k8s-mgmt-pod:local
```
Then connect either with native SSH:
```bash
ssh admin@localhost -p 2222
```
Or in a browser at `http://localhost:3000/`.
## Run with password fallback
```bash
docker run --rm \
-p 2222:22 \
-p 3000:3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \
k8s-mgmt-pod:local
```
If that fails with a message like `unable to upgrade to tcp, received 500`, the container may still be fine and the error may be coming from the attached run path in your Docker frontend. Try detached mode and inspect the logs instead:
```bash
docker run -d --name k8s-mgmt-pod \
-p 2222:22 \
-p 3000:3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \
k8s-mgmt-pod:local
docker logs -f k8s-mgmt-pod
```
If Docker reports `bind: address already in use`, change the host-side ports or let Docker choose them automatically:
```bash
docker run --rm \
-p 127.0.0.1::22 \
-p 127.0.0.1::3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \
k8s-mgmt-pod:local
```
## Run with both SSH key and password enabled
```bash
docker run --rm \
-p 2222:22 \
-p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
-e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \
-e USER_PASSWORD='change-this-user-password' \
k8s-mgmt-pod:local
```
With that configuration, each user can authenticate with either their SSH key or their password.
## Run with generated password fallback
If you omit both the public-key and password variables for a user, the container generates a random password for that user and prints it once on startup.
```bash
docker run --rm \
-p 2222:22 \
-p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
k8s-mgmt-pod:local
```
Retrieve the generated password from logs immediately:
```bash
docker logs <container-name>
```
## CI publishing
The repository includes [.github/workflows/build-workflow.yaml](.github/workflows/build-workflow.yaml), modeled on the requested reference workflow. It:
- runs weekly, on `main`, on matching tags, on pull requests, and manually
- detects GitHub vs Forgejo and picks the correct registry and token
- builds `linux/amd64` only
- pushes only for non-PR runs
- tags `main` as `latest`, date-based, and date-plus-short-sha
- tags non-`main` branches with `dev-<branch>-*`
## Kubernetes deployment
Plain manifests live under [k8s/README.md](k8s/README.md) and the rest of the [k8s](k8s) directory.