# 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 ``` ## 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--*` ## Kubernetes deployment Plain manifests live under [k8s/README.md](k8s/README.md) and the rest of the [k8s](k8s) directory.