k8s-mgmt-pod/README.md
Michael Trip df0eaf0a21
All checks were successful
Build k8s-mgmt-pod image / Build & Push (push) Successful in 1m23s
Add kubectx and kubens support to Dockerfile and bash completion scripts
2026-06-29 13:40:34 +02:00

216 lines
8.2 KiB
Markdown

# 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
- `kubectx` and `kubens` preinstalled from a pinned upstream release
- `jq`, `yq`, and `vim` preinstalled from Debian packages
- `kubectl`, `helm`, `kubectx`, and `kubens` bash autocompletion enabled for both `admin` and `user`
- local `openssh-server` on port `2222`
- 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
- `2222`: 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.
When both are available, the bootstrap kubeconfig is applied after the service-account kubeconfig and therefore takes precedence.
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:2222 \
-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:2222 \
-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:2222 \
-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::2222 \
-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:2222 \
-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:2222 \
-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.