Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

16 changed files with 70 additions and 405 deletions

View file

@ -5,6 +5,9 @@ on:
- cron: '0 3 * * 6' - cron: '0 3 * * 6'
push: push:
branches: ["main"] branches: ["main"]
paths:
- "Dockerfile"
- ".github/workflows/build-workflow.yaml"
tags: ["v*.*.*"] tags: ["v*.*.*"]
pull_request: pull_request:
branches: ["main"] branches: ["main"]

View file

@ -1,9 +1,8 @@
ARG NODE_IMAGE=node:22-trixie-slim ARG NODE_IMAGE=node:22-bookworm-slim
FROM ${NODE_IMAGE} AS webssh2-builder FROM ${NODE_IMAGE} AS webssh2-builder
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# renovate: datasource=github-releases depName=billchurch/webssh2
ARG WEBSSH2_VERSION=5.0.1 ARG WEBSSH2_VERSION=5.0.1
RUN apt-get update \ RUN apt-get update \
@ -20,31 +19,19 @@ RUN apt-get update \
&& rm -f "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \ && rm -f "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \
&& npm cache clean --force && npm cache clean --force
FROM debian:trixie-slim FROM debian:bookworm-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 \ ENV DEBIAN_FRONTEND=noninteractive \
LANG=en_US.UTF-8 \
LANGUAGE=en_US:en \
LC_ALL=en_US.UTF-8 \
WEB_SSH_PORT=3000 \ WEB_SSH_PORT=3000 \
WEBSSH2_LISTEN_PORT=3001 \ WEBSSH2_LISTEN_PORT=3001 \
WEBSSH2_SSH_HOST=127.0.0.1 \ WEBSSH2_SSH_HOST=127.0.0.1 \
WEBSSH2_SSH_PORT=2222 \ WEBSSH2_SSH_PORT=22 \
SSH_PORT=2222 \
WEBSSH2_HEADER_TEXT=k8s-mgmt-pod \ WEBSSH2_HEADER_TEXT=k8s-mgmt-pod \
WEBSSH2_SSH_HOSTKEY_ENABLED=false WEBSSH2_SSH_HOSTKEY_ENABLED=false
@ -56,12 +43,7 @@ RUN apt-get update \
bash-completion \ bash-completion \
ca-certificates \ ca-certificates \
curl \ curl \
jq \
locales \
unzip \
tar \ tar \
vim \
yq \
nginx-light \ nginx-light \
netcat-openbsd \ netcat-openbsd \
openssh-client \ openssh-client \
@ -93,23 +75,10 @@ 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" \ && 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 \ && tar -xzf "/tmp/k9s_Linux_amd64.tar.gz" -C /tmp \
&& install -m 0755 /tmp/k9s /usr/local/bin/k9s \ && 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 \ && 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 \ && kubectl completion bash > /etc/bash_completion.d/kubectl \
&& helm completion bash > /etc/bash_completion.d/helm \ && helm completion bash > /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 \ && chmod 0644 /etc/bash_completion.d/kubectl /etc/bash_completion.d/helm \
&& 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 -f /tmp/kubectl.sha256 \
&& rm -rf /tmp/linux-amd64 \ && rm -rf /tmp/linux-amd64 \
&& rm -f \ && rm -f \
@ -118,12 +87,8 @@ RUN apt-get update \
/tmp/k9s \ /tmp/k9s \
/tmp/README.md \ /tmp/README.md \
/tmp/LICENSE \ /tmp/LICENSE \
/tmp/kubelogin_linux_amd64.zip \
/tmp/kubelogin_linux_amd64.zip.sha256 \
/tmp/k9s_Linux_amd64.tar.gz \ /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/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 COPY docker/nginx/webssh.conf /etc/nginx/conf.d/webssh.conf
@ -134,9 +99,9 @@ COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod 0755 /usr/local/bin/entrypoint.sh \ RUN chmod 0755 /usr/local/bin/entrypoint.sh \
&& chmod 0644 /etc/profile.d/20-k8s-tools-completion.sh && chmod 0644 /etc/profile.d/20-k8s-tools-completion.sh
EXPOSE 2222 3000 EXPOSE 22 3000
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
CMD nc -z 127.0.0.1 "${SSH_PORT:-2222}" || exit 1 CMD nc -z 127.0.0.1 22 || exit 1
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]

View file

@ -6,10 +6,8 @@
- `kubectl` preinstalled from a pinned upstream binary release - `kubectl` preinstalled from a pinned upstream binary release
- `helm` preinstalled from a pinned upstream binary release - `helm` preinstalled from a pinned upstream binary release
- `k9s` preinstalled from a pinned GitHub release tarball - `k9s` preinstalled from a pinned GitHub release tarball
- `kubectx` and `kubens` preinstalled from a pinned upstream release - `kubectl` and `helm` bash autocompletion enabled for both `admin` and `user`
- `jq`, `yq`, and `vim` preinstalled from Debian packages - local `openssh-server` on port `22`
- `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 browser-based SSH client on port `3000`
- a custom ASCII-art MOTD on login - a custom ASCII-art MOTD on login
- two provisioned users: `admin` and `user` - two provisioned users: `admin` and `user`
@ -22,7 +20,7 @@ One caveat is worth stating clearly: browser-based SSH tools do not keep the pri
## Ports ## Ports
- `2222`: native SSH into the container's local `sshd` - `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 - `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 ## Runtime environment variables
@ -73,8 +71,6 @@ 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. 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: Start it with:
```bash ```bash
@ -117,7 +113,7 @@ If `${HOME}/.kube/config` is missing on the host, the container logs that bootst
```bash ```bash
docker run --rm \ docker run --rm \
-p 2222:2222 \ -p 2222:22 \
-p 3000:3000 \ -p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
-e USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \ -e USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \
@ -136,7 +132,7 @@ Or in a browser at `http://localhost:3000/`.
```bash ```bash
docker run --rm \ docker run --rm \
-p 2222:2222 \ -p 2222:22 \
-p 3000:3000 \ -p 3000:3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \ -e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \ -e USER_PASSWORD='change-this-user-password' \
@ -147,7 +143,7 @@ If that fails with a message like `unable to upgrade to tcp, received 500`, the
```bash ```bash
docker run -d --name k8s-mgmt-pod \ docker run -d --name k8s-mgmt-pod \
-p 2222:2222 \ -p 2222:22 \
-p 3000:3000 \ -p 3000:3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \ -e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \ -e USER_PASSWORD='change-this-user-password' \
@ -160,7 +156,7 @@ If Docker reports `bind: address already in use`, change the host-side ports or
```bash ```bash
docker run --rm \ docker run --rm \
-p 127.0.0.1::2222 \ -p 127.0.0.1::22 \
-p 127.0.0.1::3000 \ -p 127.0.0.1::3000 \
-e ADMIN_PASSWORD='change-this-admin-password' \ -e ADMIN_PASSWORD='change-this-admin-password' \
-e USER_PASSWORD='change-this-user-password' \ -e USER_PASSWORD='change-this-user-password' \
@ -171,7 +167,7 @@ docker run --rm \
```bash ```bash
docker run --rm \ docker run --rm \
-p 2222:2222 \ -p 2222:22 \
-p 3000:3000 \ -p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
-e ADMIN_PASSWORD='change-this-admin-password' \ -e ADMIN_PASSWORD='change-this-admin-password' \
@ -188,7 +184,7 @@ If you omit both the public-key and password variables for a user, the container
```bash ```bash
docker run --rm \ docker run --rm \
-p 2222:2222 \ -p 2222:22 \
-p 3000:3000 \ -p 3000:3000 \
-e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \ -e ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
k8s-mgmt-pod:local k8s-mgmt-pod:local

View file

@ -6,7 +6,7 @@ services:
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: k8s-mgmt-pod container_name: k8s-mgmt-pod
ports: ports:
- "${HOST_SSH_PORT:-2222}:2222" - "${HOST_SSH_PORT:-2222}:22"
- "${HOST_WEBSSH_PORT:-3000}:3000" - "${HOST_WEBSSH_PORT:-3000}:3000"
environment: environment:
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-this-admin-password} ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-this-admin-password}

View file

@ -1,13 +1,13 @@
╔════════════════════════════════════════════════════════════════╗ ╔════════════════════════════════════════════════════════════════╗
║ ║ ║ ║
██╗ ███████╗██╗ ██╗ ██╗ ██╗ █████╗ ███████╗
_ __ ___ ____ __ __ ____ __ __ _____ ██║ ██╔════╝██║ ██╔╝ ██║ ██╔╝██╔══██╗██╔════╝
| |/ / / _ \ / ___| | \/ | / ___|| \/ ||_ _| ██║ █████╗ █████╔╝ █████╔╝ ╚█████╔╝███████╗
| ' / | (_) |\___ \ | |\/| || | _ | |\/| | | | ██║ ██╔══╝ ██╔═██╗ ██╔═██╗ ██╔══██╗╚════██║
| . \ \__, | ___) | | | | || |_| || | | | | | ███████╗██║ ██║ ██╗ ██║ ██╗╚█████╔╝███████║
|_|\_\ /_/ |____/ |_| |_| \____||_| |_| |_| ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚════╝ ╚══════╝
║ Kubernetes Management Pod - Jump Box ║ Kubernetes Management Pod - Jump Box ║
║ ║ ║ ║
╠════════════════════════════════════════════════════════════════╣ ╠════════════════════════════════════════════════════════════════╣
║ Toolbelt ║ ║ Toolbelt ║
@ -15,7 +15,7 @@
║ - lfk : LFK kubernetes management ║ ║ - lfk : LFK kubernetes management ║
║ - k9s : interactive cluster navigation ║ ║ - k9s : interactive cluster navigation ║
║ - helm : chart install, upgrade, and rollback ║ ║ - helm : chart install, upgrade, and rollback ║
║ - plus : kubectx, kubens, jq, yq, vim, and more ║ ║ - plus : kubectx, kubens, jq, yq, and more
║ ║ ║ ║
╠════════════════════════════════════════════════════════════════╣ ╠════════════════════════════════════════════════════════════════╣
║ Quick start ║ ║ Quick start ║

View file

@ -13,14 +13,6 @@ if [ -n "${BASH_VERSION:-}" ]; then
if [ -r /etc/bash_completion.d/helm ]; then if [ -r /etc/bash_completion.d/helm ]; then
. /etc/bash_completion.d/helm . /etc/bash_completion.d/helm
fi 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 fi
alias k=kubectl alias k=kubectl

View file

@ -1,3 +1,4 @@
Port 22
Protocol 2 Protocol 2
PermitRootLogin no PermitRootLogin no
PubkeyAuthentication yes PubkeyAuthentication yes

View file

@ -4,20 +4,12 @@ set -Eeuo pipefail
STATE_DIR="/var/lib/k8s-mgmt-pod" STATE_DIR="/var/lib/k8s-mgmt-pod"
GENERATED_PASSWORDS_FILE="${STATE_DIR}/generated-passwords.env" GENERATED_PASSWORDS_FILE="${STATE_DIR}/generated-passwords.env"
SSH_DYNAMIC_CONFIG="/etc/ssh/sshd_config.d/20-k8s-mgmt-pod-auth.conf" 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 PASSWORD_AUTH_REQUIRED=0
SSHD_PID="" SSHD_PID=""
WEBSSH2_PID="" WEBSSH2_PID=""
NGINX_PID="" NGINX_PID=""
BOOTSTRAP_KUBECONFIG="${BOOTSTRAP_KUBECONFIG:-/bootstrap/kubeconfig/config}" BOOTSTRAP_KUBECONFIG="${BOOTSTRAP_KUBECONFIG:-/bootstrap/kubeconfig/config}"
ENABLE_BOOTSTRAP_KUBECONFIG="${ENABLE_BOOTSTRAP_KUBECONFIG:-false}" 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() { ensure_host_keys() {
if [[ ! -f "${STATE_DIR}/ssh_host_rsa_key" ]]; then if [[ ! -f "${STATE_DIR}/ssh_host_rsa_key" ]]; then
@ -78,8 +70,7 @@ store_generated_password() {
read_generated_password() { read_generated_password() {
local username="$1" local username="$1"
# Return an empty string if no entry exists; never fail under `set -euo pipefail`. grep -E "^${username}=" "${GENERATED_PASSWORDS_FILE}" | tail -n 1 | cut -d '=' -f 2-
awk -F '=' -v username="${username}" '$1 == username { value = substr($0, index($0, "=") + 1) } END { if (value != "") print value }' "${GENERATED_PASSWORDS_FILE}"
} }
write_authorized_keys() { write_authorized_keys() {
@ -98,87 +89,12 @@ write_authorized_keys() {
install_kubeconfig_for_user() { install_kubeconfig_for_user() {
local username="$1" local username="$1"
local source_kubeconfig="$2"
local home_dir="/home/${username}" local home_dir="/home/${username}"
local kube_dir="${home_dir}/.kube" local kube_dir="${home_dir}/.kube"
local kube_config="${kube_dir}/config" local kube_config="${kube_dir}/config"
mkdir -p "${kube_dir}" mkdir -p "${kube_dir}"
mkdir -p "${kube_dir}/cache" install -m 0600 -o "${username}" -g "${username}" "${BOOTSTRAP_KUBECONFIG}" "${kube_config}"
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}" <<EOF
apiVersion: v1
kind: Config
clusters:
- name: in-cluster
cluster:
certificate-authority: ${ca_file}
server: ${server}
contexts:
- name: in-cluster
context:
cluster: in-cluster
user: service-account
namespace: ${namespace}
current-context: in-cluster
users:
- name: service-account
user:
tokenFile: ${token_file}
EOF
chmod 0600 "${generated_kubeconfig}"
install_kubeconfig_for_user admin "${generated_kubeconfig}"
install_kubeconfig_for_user user "${generated_kubeconfig}"
log "Installed generated in-cluster kubeconfig for admin and user from ${sa_dir}."
} }
install_bootstrap_kubeconfig() { install_bootstrap_kubeconfig() {
@ -191,8 +107,8 @@ install_bootstrap_kubeconfig() {
return return
fi fi
install_kubeconfig_for_user admin "${BOOTSTRAP_KUBECONFIG}" install_kubeconfig_for_user admin
install_kubeconfig_for_user user "${BOOTSTRAP_KUBECONFIG}" install_kubeconfig_for_user user
log "Installed mounted kubeconfig for admin and user." log "Installed mounted kubeconfig for admin and user."
} }
@ -270,38 +186,15 @@ render_sshd_auth_config() {
fi fi
cat > "${SSH_DYNAMIC_CONFIG}" <<EOF cat > "${SSH_DYNAMIC_CONFIG}" <<EOF
Port ${SSH_PORT}
PasswordAuthentication ${password_value} PasswordAuthentication ${password_value}
KbdInteractiveAuthentication ${keyboard_interactive_value} KbdInteractiveAuthentication ${keyboard_interactive_value}
EOF EOF
} }
persist_kubernetes_login_env() {
local kubernetes_host="${KUBERNETES_SERVICE_HOST:-kubernetes.default.svc}"
local kubernetes_https_port="${KUBERNETES_SERVICE_PORT_HTTPS:-443}"
local kubernetes_port="${KUBERNETES_SERVICE_PORT:-${kubernetes_https_port}}"
cat > "${K8S_RUNTIME_ENV_PROFILE}" <<EOF
# Generated at container start so SSH login shells see Kubernetes API environment values.
export KUBERNETES_SERVICE_HOST='${kubernetes_host}'
export KUBERNETES_SERVICE_PORT='${kubernetes_port}'
export KUBERNETES_SERVICE_PORT_HTTPS='${kubernetes_https_port}'
EOF
chmod 0644 "${K8S_RUNTIME_ENV_PROFILE}"
}
prepare_runtime() { prepare_runtime() {
mkdir -p /var/run/sshd mkdir -p /var/run/sshd
ensure_host_keys ensure_host_keys
if [[ "${ENABLE_BOOTSTRAP_KUBECONFIG}" == "true" ]]; then install_bootstrap_kubeconfig
log "Kubeconfig mode: bootstrap-only (serviceaccount kubeconfig generation disabled)."
install_bootstrap_kubeconfig
else
log "Kubeconfig mode: in-cluster serviceaccount."
install_serviceaccount_kubeconfig
fi
persist_kubernetes_login_env
configure_user_auth admin ADMIN_SSH_PUBKEY ADMIN_PASSWORD configure_user_auth admin ADMIN_SSH_PUBKEY ADMIN_PASSWORD
configure_user_auth user USER_SSH_PUBKEY USER_PASSWORD configure_user_auth user USER_SSH_PUBKEY USER_PASSWORD
render_sshd_auth_config render_sshd_auth_config
@ -318,7 +211,7 @@ start_services() {
nginx -g 'daemon off;' & nginx -g 'daemon off;' &
NGINX_PID=$! NGINX_PID=$!
log "sshd started on port ${SSH_PORT}." log "sshd started on port 22."
log "WebSSH2 started on port ${WEBSSH2_LISTEN_PORT}." log "WebSSH2 started on port ${WEBSSH2_LISTEN_PORT}."
log "nginx redirect/proxy started on port ${WEB_SSH_PORT}." log "nginx redirect/proxy started on port ${WEB_SSH_PORT}."

View file

@ -7,7 +7,6 @@ These manifests deploy `k8s-mgmt-pod` as a single-replica bastion deployment ins
1. Build and publish the image. 1. Build and publish the image.
2. Replace the placeholder image in `deployment.yaml` with a pinned tag or digest. 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`. 3. Create the real `k8s-mgmt-pod-auth` Secret instead of applying `secret.yaml.example`.
4. The pod state directory is now backed by a PVC, so SSH host keys and generated fallback passwords survive pod recreation until you delete the claim.
## Create the Secret ## Create the Secret
@ -63,7 +62,7 @@ If you prefer individual files, apply `namespace.yaml`, `serviceaccount.yaml`, `
Port-forward the service instead of exposing it externally by default: Port-forward the service instead of exposing it externally by default:
```bash ```bash
kubectl -n k8s-mgmt-pod port-forward svc/k8s-mgmt-pod 2222:2222 3000:3000 kubectl -n k8s-mgmt-pod port-forward svc/k8s-mgmt-pod 2222:22 3000:3000
``` ```
Then connect with: Then connect with:

View file

@ -16,24 +16,20 @@ spec:
spec: spec:
serviceAccountName: k8s-mgmt-pod serviceAccountName: k8s-mgmt-pod
automountServiceAccountToken: true automountServiceAccountToken: true
securityContext: securityContext:
seccompProfile: seccompProfile:
type: RuntimeDefault type: RuntimeDefault
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
containers: containers:
- name: k8s-mgmt-pod - name: k8s-mgmt-pod
image: git.alcatrash.net/michael/k8s-mgmt-pod/k8s-mgmt-pod:latest image: git.alcatrash.net/michael/k8s-mgmt-pod/k8s-mgmt-pod:v1.0.0
imagePullPolicy: Always imagePullPolicy: IfNotPresent
ports: ports:
- name: ssh - name: ssh
containerPort: 2222 containerPort: 22
protocol: TCP
- name: webssh - name: webssh
containerPort: 3000 containerPort: 3000
protocol: TCP
env: env:
- name: ADMIN_SSH_PUBKEY - name: ADMIN_SSH_PUBKEY
valueFrom: valueFrom:
@ -59,7 +55,6 @@ spec:
name: k8s-mgmt-pod-auth name: k8s-mgmt-pod-auth
key: USER_PASSWORD key: USER_PASSWORD
optional: true optional: true
resources: resources:
requests: requests:
cpu: 100m cpu: 100m
@ -67,42 +62,30 @@ spec:
limits: limits:
cpu: 500m cpu: 500m
memory: 512Mi memory: 512Mi
readinessProbe: readinessProbe:
tcpSocket: tcpSocket:
port: 2222 port: 22
initialDelaySeconds: 5 initialDelaySeconds: 5
periodSeconds: 10 periodSeconds: 10
livenessProbe: livenessProbe:
tcpSocket: tcpSocket:
port: 2222 port: 22
initialDelaySeconds: 15 initialDelaySeconds: 15
periodSeconds: 20 periodSeconds: 20
volumeMounts: volumeMounts:
- name: state - name: state
mountPath: /var/lib/k8s-mgmt-pod mountPath: /var/lib/k8s-mgmt-pod
securityContext: 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 runAsUser: 0
runAsGroup: 0 runAsGroup: 0
runAsNonRoot: false runAsNonRoot: false
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
capabilities: capabilities:
drop: drop: ["ALL"]
- ALL add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "NET_BIND_SERVICE", "SETGID", "SETUID"]
add:
- AUDIT_WRITE
- CHOWN
- DAC_OVERRIDE
- FOWNER
- SETGID
- SETUID
- SYS_CHROOT
volumes: volumes:
- name: state - name: state
persistentVolumeClaim: emptyDir: {}
claimName: k8s-mgmt-pod-state

View file

@ -5,7 +5,6 @@ resources:
- namespace.yaml - namespace.yaml
- serviceaccount.yaml - serviceaccount.yaml
- rbac.yaml - rbac.yaml
- pvc.yaml
- deployment.yaml - deployment.yaml
- service.yaml - service.yaml
- networkpolicy.yaml - networkpolicy.yaml

View file

@ -19,6 +19,6 @@ spec:
k8s-mgmt-pod-access: "true" k8s-mgmt-pod-access: "true"
ports: ports:
- protocol: TCP - protocol: TCP
port: 2222 port: 22
- protocol: TCP - protocol: TCP
port: 3000 port: 3000

View file

@ -1,10 +0,0 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: k8s-mgmt-pod-state
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi

View file

@ -1,189 +1,49 @@
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole kind: Role
metadata: metadata:
name: k8s-mgmt-pod name: k8s-mgmt-pod
rules: 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: [""] - apiGroups: [""]
resources: resources:
- configmaps - configmaps
- endpoints - endpoints
- events - events
- limitranges
- namespaces
- nodes
- persistentvolumeclaims - persistentvolumeclaims
- persistentvolumes
- pods - pods
- pods/exec
- pods/log - pods/log
- pods/portforward - secrets
- pods/proxy
- replicationcontrollers
- replicationcontrollers/scale
- resourcequotas
- serviceaccounts - serviceaccounts
- services - services
- services/proxy verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
verbs: ["*"]
- apiGroups: ["apps"] - apiGroups: ["apps"]
resources: resources:
- controllerrevisions
- daemonsets - daemonsets
- deployments - deployments
- deployments/scale
- replicasets - replicasets
- replicasets/scale
- statefulsets - statefulsets
- statefulsets/scale verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
verbs: ["*"]
- apiGroups: ["batch"] - apiGroups: ["batch"]
resources: resources:
- cronjobs - cronjobs
- jobs - jobs
verbs: ["*"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["autoscaling"]
resources:
- horizontalpodautoscalers
verbs: ["*"]
- apiGroups: ["networking.k8s.io"] - apiGroups: ["networking.k8s.io"]
resources: resources:
- ingressclasses
- ingresses - ingresses
verbs: ["*"] - networkpolicies
- apiGroups: ["extensions"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
resources:
- ingresses
verbs: ["*"]
- apiGroups: ["policy"]
resources:
- poddisruptionbudgets
verbs: ["*"]
- apiGroups: ["coordination.k8s.io"]
resources:
- leases
verbs: ["*"]
- apiGroups: ["storage.k8s.io"]
resources:
- csidrivers
- csinodes
- csistoragecapacities
- storageclasses
- volumeattachments
verbs: ["*"]
- apiGroups: ["apiextensions.k8s.io"]
resources:
- customresourcedefinitions
verbs: ["*"]
- apiGroups: ["admissionregistration.k8s.io"]
resources:
- mutatingadmissionpolicies
- mutatingwebhookconfigurations
- validatingadmissionpolicies
- validatingwebhookconfigurations
verbs: ["*"]
- apiGroups: ["metrics.k8s.io"]
resources:
- nodes
- pods
verbs: ["get", "list", "watch"]
- apiGroups: ["gateway.networking.k8s.io"]
resources:
- gatewayclasses
- gateways
- httproutes
- referencegrants
- tcproutes
- tlsroutes
- udproutes
verbs: ["*"]
- apiGroups: ["cert-manager.io"]
resources:
- certificates
- certificaterequests
- clusterissuers
- issuers
verbs: ["*"]
- apiGroups: ["monitoring.coreos.com"]
resources:
- alertmanagers
- podmonitors
- prometheuses
- prometheusrules
- servicemonitors
- thanosrulers
verbs: ["*"]
- apiGroups: ["argoproj.io"]
resources:
- applications
- appprojects
verbs: ["*"]
- apiGroups: ["traefik.containo.us", "traefik.io"]
resources:
- ingressroutes
- middlewares
- traefikservices
verbs: ["*"]
- apiGroups: ["k8s.cni.cncf.io"]
resources:
- network-attachment-definitions
verbs: ["*"]
- apiGroups: ["kubevirt.io"]
resources:
- virtualmachineinstances
- virtualmachines
verbs: ["*"]
- apiGroups: ["subresources.kubevirt.io"]
resources:
- "*"
verbs: ["*"]
- apiGroups: ["instancetype.kubevirt.io"]
resources:
- "*"
verbs: ["*"]
- apiGroups: ["cdi.kubevirt.io"]
resources:
- "*"
verbs: ["*"]
- apiGroups: ["pool.kubevirt.io"]
resources:
- "*"
verbs: ["*"]
- apiGroups: ["cluster.x-k8s.io"]
resources:
- clusters
- machinedeployments
- machinepools
- machines
- machinesets
verbs: ["*"]
- apiGroups: ["controlplane.cluster.x-k8s.io"]
resources:
- kubeadmcontrolplanes
verbs: ["*"]
- apiGroups: ["bootstrap.cluster.x-k8s.io"]
resources:
- kubeadmconfigs
- kubeadmconfigtemplates
verbs: ["*"]
- apiGroups: ["infrastructure.cluster.x-k8s.io"]
resources:
- kubevirtclusters
- kubevirtmachinetemplates
verbs: ["*"]
- apiGroups: ["addons.cluster.x-k8s.io"]
resources:
- clusterresourcesets
verbs: ["*"]
--- ---
apiVersion: rbac.authorization.k8s.io/v1 apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding kind: RoleBinding
metadata: metadata:
name: k8s-mgmt-pod-access name: k8s-mgmt-pod
subjects: subjects:
- kind: ServiceAccount - kind: ServiceAccount
name: k8s-mgmt-pod name: k8s-mgmt-pod
namespace: k8s-mgmt-pod
roleRef: roleRef:
apiGroup: rbac.authorization.k8s.io apiGroup: rbac.authorization.k8s.io
kind: ClusterRole kind: Role
name: k8s-mgmt-pod name: k8s-mgmt-pod

View file

@ -8,7 +8,7 @@ spec:
app.kubernetes.io/name: k8s-mgmt-pod app.kubernetes.io/name: k8s-mgmt-pod
ports: ports:
- name: ssh - name: ssh
port: 2222 port: 22
targetPort: ssh targetPort: ssh
protocol: TCP protocol: TCP
- name: webssh - name: webssh
@ -27,7 +27,7 @@ spec:
# app.kubernetes.io/name: k8s-mgmt-pod # app.kubernetes.io/name: k8s-mgmt-pod
# ports: # ports:
# - name: ssh # - name: ssh
# port: 2222 # port: 22
# targetPort: ssh # targetPort: ssh
# - name: webssh # - name: webssh
# port: 3000 # port: 3000

View file

@ -1,16 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": ["/^Dockerfile$/", "/Dockerfile.*/"],
"matchStrings": [
"# renovate: datasource=(?<datasource>.*?) depName=(?<depName>.*?)\\s+ARG .*?_VERSION=(?<currentValue>.*)\\s"
],
"datasourceTemplate": "{{{datasource}}}"
}
]
}