diff --git a/.github/workflows/build-workflow.yaml b/.github/workflows/build-workflow.yaml index 5df5f7a..3036ab7 100644 --- a/.github/workflows/build-workflow.yaml +++ b/.github/workflows/build-workflow.yaml @@ -5,9 +5,6 @@ on: - cron: '0 3 * * 6' push: branches: ["main"] - paths: - - "Dockerfile" - - ".github/workflows/build-workflow.yaml" tags: ["v*.*.*"] pull_request: branches: ["main"] diff --git a/Dockerfile b/Dockerfile index 70f8ad1..481e097 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -ARG NODE_IMAGE=node:22-bookworm-slim +ARG NODE_IMAGE=node:22-trixie-slim FROM ${NODE_IMAGE} AS webssh2-builder ENV DEBIAN_FRONTEND=noninteractive +# renovate: datasource=github-releases depName=billchurch/webssh2 ARG WEBSSH2_VERSION=5.0.1 RUN apt-get update \ @@ -19,19 +20,31 @@ RUN apt-get update \ && rm -f "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \ && npm cache clean --force -FROM debian:bookworm-slim +FROM debian:trixie-slim + +# renovate: datasource=github-releases depName=janosmiko/lfk +ARG LFK_VERSION=0.14.18 +# renovate: datasource=github-releases depName=kubernetes/kubernetes +ARG KUBECTL_VERSION=1.36.2 +# renovate: datasource=github-releases depName=helm/helm +ARG HELM_VERSION=3.21.2 +# renovate: datasource=github-releases depName=derailed/k9s +ARG K9S_VERSION=0.51.0 +# renovate: datasource=github-releases depName=int128/kubelogin +ARG OIDC_LOGIN_VERSION=1.36.2 +# renovate: datasource=github-tags depName=ahmetb/kubectx +ARG KUBECTX_VERSION=0.9.5 -ARG LFK_VERSION=0.14.9 -ARG KUBECTL_VERSION=1.31.0 -ARG HELM_VERSION=3.16.2 -ARG K9S_VERSION=0.40.10 -ARG WEBSSH2_VERSION=5.0.1 ENV DEBIAN_FRONTEND=noninteractive \ + LANG=en_US.UTF-8 \ + LANGUAGE=en_US:en \ + LC_ALL=en_US.UTF-8 \ WEB_SSH_PORT=3000 \ WEBSSH2_LISTEN_PORT=3001 \ WEBSSH2_SSH_HOST=127.0.0.1 \ - WEBSSH2_SSH_PORT=22 \ + WEBSSH2_SSH_PORT=2222 \ + SSH_PORT=2222 \ WEBSSH2_HEADER_TEXT=k8s-mgmt-pod \ WEBSSH2_SSH_HOSTKEY_ENABLED=false @@ -43,7 +56,12 @@ RUN apt-get update \ bash-completion \ ca-certificates \ curl \ + jq \ + locales \ + unzip \ tar \ + vim \ + yq \ nginx-light \ netcat-openbsd \ openssh-client \ @@ -75,10 +93,23 @@ RUN apt-get update \ && curl -fsSLo "/tmp/k9s_Linux_amd64.tar.gz" "https://github.com/derailed/k9s/releases/download/v${K9S_VERSION}/k9s_Linux_amd64.tar.gz" \ && tar -xzf "/tmp/k9s_Linux_amd64.tar.gz" -C /tmp \ && install -m 0755 /tmp/k9s /usr/local/bin/k9s \ + && curl -fsSLo "/tmp/kubelogin_linux_amd64.zip" "https://github.com/int128/kubelogin/releases/download/v${OIDC_LOGIN_VERSION}/kubelogin_linux_amd64.zip" \ + && curl -fsSLo "/tmp/kubelogin_linux_amd64.zip.sha256" "https://github.com/int128/kubelogin/releases/download/v${OIDC_LOGIN_VERSION}/kubelogin_linux_amd64.zip.sha256" \ + && sed 's#\*kubelogin_linux_amd64.zip$#*/tmp/kubelogin_linux_amd64.zip#' /tmp/kubelogin_linux_amd64.zip.sha256 | sha256sum -c - \ + && unzip -p /tmp/kubelogin_linux_amd64.zip kubelogin > /usr/local/bin/kubectl-oidc_login \ + && chmod 0755 /usr/local/bin/kubectl-oidc_login \ + && curl -fsSLo /usr/local/bin/kubectx "https://raw.githubusercontent.com/ahmetb/kubectx/v${KUBECTX_VERSION}/kubectx" \ + && curl -fsSLo /usr/local/bin/kubens "https://raw.githubusercontent.com/ahmetb/kubectx/v${KUBECTX_VERSION}/kubens" \ + && chmod 0755 /usr/local/bin/kubectx /usr/local/bin/kubens \ && mkdir -p /etc/bash_completion.d \ + && curl -fsSLo /etc/bash_completion.d/kubectx "https://raw.githubusercontent.com/ahmetb/kubectx/v${KUBECTX_VERSION}/completion/kubectx.bash" \ + && curl -fsSLo /etc/bash_completion.d/kubens "https://raw.githubusercontent.com/ahmetb/kubectx/v${KUBECTX_VERSION}/completion/kubens.bash" \ && kubectl completion bash > /etc/bash_completion.d/kubectl \ && helm completion bash > /etc/bash_completion.d/helm \ - && chmod 0644 /etc/bash_completion.d/kubectl /etc/bash_completion.d/helm \ + && chmod 0644 /etc/bash_completion.d/kubectl /etc/bash_completion.d/helm /etc/bash_completion.d/kubectx /etc/bash_completion.d/kubens \ + && sed -i 's/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen \ + && locale-gen en_US.UTF-8 \ + && update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 \ && rm -f /tmp/kubectl.sha256 \ && rm -rf /tmp/linux-amd64 \ && rm -f \ @@ -87,8 +118,12 @@ RUN apt-get update \ /tmp/k9s \ /tmp/README.md \ /tmp/LICENSE \ + /tmp/kubelogin_linux_amd64.zip \ + /tmp/kubelogin_linux_amd64.zip.sha256 \ /tmp/k9s_Linux_amd64.tar.gz \ - && rm -f /etc/ssh/ssh_host_* + && rm -f /etc/ssh/ssh_host_* \ + && apt-get clean + COPY docker/sshd/10-lfk-base.conf /etc/ssh/sshd_config.d/10-lfk-base.conf COPY docker/nginx/webssh.conf /etc/nginx/conf.d/webssh.conf @@ -99,9 +134,9 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh RUN chmod 0755 /usr/local/bin/entrypoint.sh \ && chmod 0644 /etc/profile.d/20-k8s-tools-completion.sh -EXPOSE 22 3000 +EXPOSE 2222 3000 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ - CMD nc -z 127.0.0.1 22 || exit 1 + CMD nc -z 127.0.0.1 "${SSH_PORT:-2222}" || exit 1 -ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] \ No newline at end of file diff --git a/README.md b/README.md index 308526d..ca220cc 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,10 @@ - `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` +- `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` @@ -20,7 +22,7 @@ One caveat is worth stating clearly: browser-based SSH tools do not keep the pri ## Ports -- `22`: native SSH into the container's local `sshd` +- `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 @@ -71,6 +73,8 @@ The included [docker-compose.yaml](docker-compose.yaml) enables kubeconfig boots 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 @@ -113,7 +117,7 @@ If `${HOME}/.kube/config` is missing on the host, the container logs that bootst ```bash docker run --rm \ - -p 2222:22 \ + -p 2222:2222 \ -p 3000:3000 \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ -e USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \ @@ -132,7 +136,7 @@ Or in a browser at `http://localhost:3000/`. ```bash docker run --rm \ - -p 2222:22 \ + -p 2222:2222 \ -p 3000:3000 \ -e ADMIN_PASSWORD='change-this-admin-password' \ -e USER_PASSWORD='change-this-user-password' \ @@ -143,7 +147,7 @@ If that fails with a message like `unable to upgrade to tcp, received 500`, the ```bash docker run -d --name k8s-mgmt-pod \ - -p 2222:22 \ + -p 2222:2222 \ -p 3000:3000 \ -e ADMIN_PASSWORD='change-this-admin-password' \ -e USER_PASSWORD='change-this-user-password' \ @@ -156,7 +160,7 @@ If Docker reports `bind: address already in use`, change the host-side ports or ```bash docker run --rm \ - -p 127.0.0.1::22 \ + -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' \ @@ -167,7 +171,7 @@ docker run --rm \ ```bash docker run --rm \ - -p 2222:22 \ + -p 2222:2222 \ -p 3000:3000 \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ -e ADMIN_PASSWORD='change-this-admin-password' \ @@ -184,7 +188,7 @@ If you omit both the public-key and password variables for a user, the container ```bash docker run --rm \ - -p 2222:22 \ + -p 2222:2222 \ -p 3000:3000 \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ k8s-mgmt-pod:local diff --git a/docker-compose.yaml b/docker-compose.yaml index 4f81d2e..95ff74a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,7 +6,7 @@ services: dockerfile: Dockerfile container_name: k8s-mgmt-pod ports: - - "${HOST_SSH_PORT:-2222}:22" + - "${HOST_SSH_PORT:-2222}:2222" - "${HOST_WEBSSH_PORT:-3000}:3000" environment: ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-this-admin-password} diff --git a/docker/motd b/docker/motd index 134af58..859b382 100644 --- a/docker/motd +++ b/docker/motd @@ -1,13 +1,13 @@ ╔════════════════════════════════════════════════════════════════╗ ║ ║ - ║ ██╗ ███████╗██╗ ██╗ ██╗ ██╗ █████╗ ███████╗ ║ - ║ ██║ ██╔════╝██║ ██╔╝ ██║ ██╔╝██╔══██╗██╔════╝ ║ - ║ ██║ █████╗ █████╔╝ █████╔╝ ╚█████╔╝███████╗ ║ - ║ ██║ ██╔══╝ ██╔═██╗ ██╔═██╗ ██╔══██╗╚════██║ ║ - ║ ███████╗██║ ██║ ██╗ ██║ ██╗╚█████╔╝███████║ ║ - ║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚════╝ ╚══════╝ ║ - ║ Kubernetes Management Pod - Jump Box ║ + ║ ║ + ║ _ __ ___ ____ __ __ ____ __ __ _____ ║ + ║ | |/ / / _ \ / ___| | \/ | / ___|| \/ ||_ _| ║ + ║ | ' / | (_) |\___ \ | |\/| || | _ | |\/| | | | ║ + ║ | . \ \__, | ___) | | | | || |_| || | | | | | ║ + ║ |_|\_\ /_/ |____/ |_| |_| \____||_| |_| |_| ║ + ║ Kubernetes Management Pod - Jump Box ║ ║ ║ ╠════════════════════════════════════════════════════════════════╣ ║ Toolbelt ║ @@ -15,7 +15,7 @@ ║ - lfk : LFK kubernetes management ║ ║ - k9s : interactive cluster navigation ║ ║ - helm : chart install, upgrade, and rollback ║ - ║ - plus : kubectx, kubens, jq, yq, and more ║ + ║ - plus : kubectx, kubens, jq, yq, vim, and more ║ ║ ║ ╠════════════════════════════════════════════════════════════════╣ ║ Quick start ║ diff --git a/docker/profile/20-k8s-tools-completion.sh b/docker/profile/20-k8s-tools-completion.sh index b9092e6..5cb4a72 100644 --- a/docker/profile/20-k8s-tools-completion.sh +++ b/docker/profile/20-k8s-tools-completion.sh @@ -13,6 +13,14 @@ if [ -n "${BASH_VERSION:-}" ]; then if [ -r /etc/bash_completion.d/helm ]; then . /etc/bash_completion.d/helm fi + + if [ -r /etc/bash_completion.d/kubectx ]; then + . /etc/bash_completion.d/kubectx + fi + + if [ -r /etc/bash_completion.d/kubens ]; then + . /etc/bash_completion.d/kubens + fi fi alias k=kubectl diff --git a/docker/sshd/10-lfk-base.conf b/docker/sshd/10-lfk-base.conf index 5b59dd1..cf982a6 100644 --- a/docker/sshd/10-lfk-base.conf +++ b/docker/sshd/10-lfk-base.conf @@ -1,4 +1,3 @@ -Port 22 Protocol 2 PermitRootLogin no PubkeyAuthentication yes diff --git a/entrypoint.sh b/entrypoint.sh index 22436d7..0253f38 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -4,12 +4,20 @@ set -Eeuo pipefail STATE_DIR="/var/lib/k8s-mgmt-pod" GENERATED_PASSWORDS_FILE="${STATE_DIR}/generated-passwords.env" SSH_DYNAMIC_CONFIG="/etc/ssh/sshd_config.d/20-k8s-mgmt-pod-auth.conf" +K8S_RUNTIME_ENV_PROFILE="/etc/profile.d/30-kubernetes-runtime-env.sh" PASSWORD_AUTH_REQUIRED=0 SSHD_PID="" WEBSSH2_PID="" NGINX_PID="" BOOTSTRAP_KUBECONFIG="${BOOTSTRAP_KUBECONFIG:-/bootstrap/kubeconfig/config}" ENABLE_BOOTSTRAP_KUBECONFIG="${ENABLE_BOOTSTRAP_KUBECONFIG:-false}" +KUBERNETES_SECRETS_DIR="${KUBERNETES_SECRETS_DIR:-/var/run/kubernetes.io}" +SSH_PORT="${SSH_PORT:-2222}" + +if [[ -z "${WEBSSH2_SSH_PORT:-}" ]]; then + WEBSSH2_SSH_PORT="${SSH_PORT}" +fi +export WEBSSH2_SSH_PORT ensure_host_keys() { if [[ ! -f "${STATE_DIR}/ssh_host_rsa_key" ]]; then @@ -70,7 +78,8 @@ store_generated_password() { read_generated_password() { local username="$1" - grep -E "^${username}=" "${GENERATED_PASSWORDS_FILE}" | tail -n 1 | cut -d '=' -f 2- + # Return an empty string if no entry exists; never fail under `set -euo pipefail`. + awk -F '=' -v username="${username}" '$1 == username { value = substr($0, index($0, "=") + 1) } END { if (value != "") print value }' "${GENERATED_PASSWORDS_FILE}" } write_authorized_keys() { @@ -89,12 +98,87 @@ write_authorized_keys() { install_kubeconfig_for_user() { local username="$1" + local source_kubeconfig="$2" local home_dir="/home/${username}" local kube_dir="${home_dir}/.kube" local kube_config="${kube_dir}/config" mkdir -p "${kube_dir}" - install -m 0600 -o "${username}" -g "${username}" "${BOOTSTRAP_KUBECONFIG}" "${kube_config}" + mkdir -p "${kube_dir}/cache" + chown -R "${username}:${username}" "${kube_dir}" + chmod 0700 "${kube_dir}" + install -m 0600 -o "${username}" -g "${username}" "${source_kubeconfig}" "${kube_config}" +} + +resolve_kubernetes_secrets_dir() { + local configured_dir="${KUBERNETES_SECRETS_DIR}" + local fallback_dir="/var/run/secrets/kubernetes.io/serviceaccount" + + if [[ -f "${configured_dir}/token" && -f "${configured_dir}/ca.crt" && -f "${configured_dir}/namespace" ]]; then + printf '%s\n' "${configured_dir}" + return 0 + fi + + if [[ "${configured_dir}" != "${fallback_dir}" ]] \ + && [[ -f "${fallback_dir}/token" && -f "${fallback_dir}/ca.crt" && -f "${fallback_dir}/namespace" ]]; then + printf '%s\n' "${fallback_dir}" + return 0 + fi + + return 1 +} + +install_serviceaccount_kubeconfig() { + local sa_dir="" + local token_file="" + local ca_file="" + local namespace_file="" + local namespace="" + local kubernetes_host="${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}" + local kubernetes_port="${KUBERNETES_SERVICE_PORT_HTTPS:-443}" + local server="" + local generated_kubeconfig="${STATE_DIR}/incluster.kubeconfig" + + if ! sa_dir="$(resolve_kubernetes_secrets_dir)"; then + log "Skipping in-cluster kubeconfig generation because no service-account mount was found in ${KUBERNETES_SECRETS_DIR} or /var/run/secrets/kubernetes.io/serviceaccount." + return + fi + + token_file="${sa_dir}/token" + ca_file="${sa_dir}/ca.crt" + namespace_file="${sa_dir}/namespace" + namespace="$(tr -d '\n' < "${namespace_file}")" + if [[ -z "${namespace}" ]]; then + namespace="default" + fi + + server="https://${kubernetes_host}:${kubernetes_port}" + + cat > "${generated_kubeconfig}" < "${SSH_DYNAMIC_CONFIG}" < "${K8S_RUNTIME_ENV_PROFILE}" <.*?) depName=(?.*?)\\s+ARG .*?_VERSION=(?.*)\\s" + ], + "datasourceTemplate": "{{{datasource}}}" + } + ] +} \ No newline at end of file