initial commit
All checks were successful
Build k8s-mgmt-pod image / Build & Push (push) Successful in 2m21s
All checks were successful
Build k8s-mgmt-pod image / Build & Push (push) Successful in 2m21s
This commit is contained in:
commit
4dbcde381f
20 changed files with 1151 additions and 0 deletions
7
.env.example
Normal file
7
.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
ADMIN_PASSWORD=change-this-admin-password
|
||||||
|
USER_PASSWORD=change-this-user-password
|
||||||
|
ADMIN_SSH_PUBKEY=
|
||||||
|
USER_SSH_PUBKEY=
|
||||||
|
HOST_SSH_PORT=2222
|
||||||
|
HOST_WEBSSH_PORT=3000
|
||||||
|
ENABLE_BOOTSTRAP_KUBECONFIG=true
|
||||||
156
.github/workflows/build-workflow.yaml
vendored
Normal file
156
.github/workflows/build-workflow.yaml
vendored
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
name: Build k8s-mgmt-pod image
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 3 * * 6'
|
||||||
|
push:
|
||||||
|
branches: ["main"]
|
||||||
|
paths:
|
||||||
|
- "Dockerfile"
|
||||||
|
- ".github/workflows/build-workflow.yaml"
|
||||||
|
tags: ["v*.*.*"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_BASE_NAME: k8s-mgmt-pod
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build & Push
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Detect platform & registry
|
||||||
|
id: platform
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.server_url }}" == "https://github.com" ]]; then
|
||||||
|
echo "registry=ghcr.io" >> $GITHUB_OUTPUT
|
||||||
|
echo "name=GitHub" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
DOMAIN=$(echo "${{ github.server_url }}" | sed 's|https://||')
|
||||||
|
echo "registry=${DOMAIN}" >> $GITHUB_OUTPUT
|
||||||
|
echo "name=Forgejo (${DOMAIN})" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Resolve image name
|
||||||
|
id: image
|
||||||
|
run: |
|
||||||
|
REPO=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
|
||||||
|
echo "name=${{ steps.platform.outputs.registry }}/${REPO}/${{ env.IMAGE_BASE_NAME }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate build metadata
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
BUILD_DATE_SHORT=$(date -u +'%Y%m%d')
|
||||||
|
SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
BRANCH="${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}"
|
||||||
|
BRANCH_CLEAN=$(echo "$BRANCH" | sed 's/[^a-zA-Z0-9._-]/-/g')
|
||||||
|
echo "build_date=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_date_short=${BUILD_DATE_SHORT}" >> $GITHUB_OUTPUT
|
||||||
|
echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch=${BRANCH}" >> $GITHUB_OUTPUT
|
||||||
|
echo "branch_clean=${BRANCH_CLEAN}" >> $GITHUB_OUTPUT
|
||||||
|
echo "build_version=${BUILD_DATE_SHORT}-${SHORT_SHA}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Generate Docker tags
|
||||||
|
id: tags
|
||||||
|
run: |
|
||||||
|
IMAGE="${{ steps.image.outputs.name }}"
|
||||||
|
BRANCH="${{ steps.meta.outputs.branch }}"
|
||||||
|
VERSION="${{ steps.meta.outputs.build_version }}"
|
||||||
|
DATE="${{ steps.meta.outputs.build_date_short }}"
|
||||||
|
BRANCH_CLEAN="${{ steps.meta.outputs.branch_clean }}"
|
||||||
|
|
||||||
|
if [[ "$BRANCH" == "main" ]]; then
|
||||||
|
TAGS="${IMAGE}:latest
|
||||||
|
${IMAGE}:${VERSION}
|
||||||
|
${IMAGE}:${DATE}"
|
||||||
|
else
|
||||||
|
TAGS="${IMAGE}:dev-${BRANCH_CLEAN}-latest
|
||||||
|
${IMAGE}:dev-${BRANCH_CLEAN}-${VERSION}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||||
|
echo "$TAGS" >> $GITHUB_OUTPUT
|
||||||
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
if: github.event_name != 'pull_request' && github.server_url == 'https://github.com'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Log in to Forgejo Container Registry
|
||||||
|
if: github.event_name != 'pull_request' && github.server_url != 'https://github.com'
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ steps.platform.outputs.registry }}
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.FORGEJOTOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: linux/amd64
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
tags: ${{ steps.tags.outputs.tags }}
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.title=k8s-mgmt-pod
|
||||||
|
org.opencontainers.image.description=Kubernetes management container with lfk and SSH access
|
||||||
|
org.opencontainers.image.url=${{ github.server_url }}/${{ github.repository }}
|
||||||
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
org.opencontainers.image.created=${{ steps.meta.outputs.build_date }}
|
||||||
|
org.opencontainers.image.version=${{ steps.meta.outputs.build_version }}
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
|
||||||
|
- name: Job summary
|
||||||
|
if: always()
|
||||||
|
run: |
|
||||||
|
STATUS="${{ job.status }}"
|
||||||
|
if [[ "$STATUS" == "success" ]]; then
|
||||||
|
ICON="✅"
|
||||||
|
else
|
||||||
|
ICON="❌"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "# ${ICON} Build ${STATUS^}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| | |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Platform** | ${{ steps.platform.outputs.name }} |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Registry** | \`${{ steps.platform.outputs.registry }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Image** | \`${{ steps.image.outputs.name }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Branch** | \`${{ steps.meta.outputs.branch }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Commit** | \`${{ steps.meta.outputs.short_sha }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Version** | \`${{ steps.meta.outputs.build_version }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "| **Built at** | \`${{ steps.meta.outputs.build_date }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "" >> $GITHUB_STEP_SUMMARY
|
||||||
|
|
||||||
|
if [[ "${{ github.event_name }}" != "pull_request" ]]; then
|
||||||
|
echo "### Published tags" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo "${{ steps.tags.outputs.tags }}" >> $GITHUB_STEP_SUMMARY
|
||||||
|
echo '```' >> $GITHUB_STEP_SUMMARY
|
||||||
|
else
|
||||||
|
echo "> Built but **not pushed** (pull request)" >> $GITHUB_STEP_SUMMARY
|
||||||
|
fi
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
||||||
107
Dockerfile
Normal file
107
Dockerfile
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
ARG NODE_IMAGE=node:22-bookworm-slim
|
||||||
|
FROM ${NODE_IMAGE} AS webssh2-builder
|
||||||
|
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
ARG WEBSSH2_VERSION=5.0.1
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates curl \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /opt/webssh2 \
|
||||||
|
&& curl -fsSLo "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" "https://github.com/billchurch/webssh2/releases/download/webssh2-server-v${WEBSSH2_VERSION}/webssh2-${WEBSSH2_VERSION}.tar.gz" \
|
||||||
|
&& curl -fsSLo "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" "https://github.com/billchurch/webssh2/releases/download/webssh2-server-v${WEBSSH2_VERSION}/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \
|
||||||
|
&& cd /tmp \
|
||||||
|
&& sha256sum -c "webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \
|
||||||
|
&& tar -xzf "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" -C /opt/webssh2 \
|
||||||
|
&& cd /opt/webssh2 \
|
||||||
|
&& npm ci --omit=dev \
|
||||||
|
&& rm -f "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz" "/tmp/webssh2-${WEBSSH2_VERSION}.tar.gz.sha256" \
|
||||||
|
&& npm cache clean --force
|
||||||
|
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
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 \
|
||||||
|
WEB_SSH_PORT=3000 \
|
||||||
|
WEBSSH2_LISTEN_PORT=3001 \
|
||||||
|
WEBSSH2_SSH_HOST=127.0.0.1 \
|
||||||
|
WEBSSH2_SSH_PORT=22 \
|
||||||
|
WEBSSH2_HEADER_TEXT=k8s-mgmt-pod \
|
||||||
|
WEBSSH2_SSH_HOSTKEY_ENABLED=false
|
||||||
|
|
||||||
|
COPY --from=webssh2-builder /usr/local /usr/local
|
||||||
|
COPY --from=webssh2-builder /opt/webssh2 /opt/webssh2
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
bash-completion \
|
||||||
|
ca-certificates \
|
||||||
|
curl \
|
||||||
|
tar \
|
||||||
|
nginx-light \
|
||||||
|
netcat-openbsd \
|
||||||
|
openssh-client \
|
||||||
|
openssh-server \
|
||||||
|
openssl \
|
||||||
|
sudo \
|
||||||
|
tini \
|
||||||
|
&& rm -rf /var/lib/apt/lists/* \
|
||||||
|
&& mkdir -p /var/run/sshd /var/lib/k8s-mgmt-pod /etc/ssh/sshd_config.d \
|
||||||
|
&& useradd --create-home --shell /bin/bash admin \
|
||||||
|
&& useradd --create-home --shell /bin/bash user \
|
||||||
|
&& printf 'admin ALL=(ALL) NOPASSWD:ALL\n' > /etc/sudoers.d/90-admin-nopasswd \
|
||||||
|
&& chmod 0440 /etc/sudoers.d/90-admin-nopasswd \
|
||||||
|
&& curl -fsSLo "/tmp/lfk_${LFK_VERSION}_linux_amd64.deb" "https://github.com/janosmiko/lfk/releases/download/v${LFK_VERSION}/lfk_${LFK_VERSION}_linux_amd64.deb" \
|
||||||
|
&& curl -fsSLo /tmp/lfk.checksums "https://github.com/janosmiko/lfk/releases/download/v${LFK_VERSION}/checksums.txt" \
|
||||||
|
&& cd /tmp \
|
||||||
|
&& grep " lfk_${LFK_VERSION}_linux_amd64.deb$" /tmp/lfk.checksums | sha256sum -c - \
|
||||||
|
&& dpkg -i "/tmp/lfk_${LFK_VERSION}_linux_amd64.deb" \
|
||||||
|
&& rm -f "/tmp/lfk_${LFK_VERSION}_linux_amd64.deb" /tmp/lfk.checksums \
|
||||||
|
&& curl -fsSLo /usr/local/bin/kubectl "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl" \
|
||||||
|
&& curl -fsSLo /tmp/kubectl.sha256 "https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl.sha256" \
|
||||||
|
&& echo "$(cat /tmp/kubectl.sha256) /usr/local/bin/kubectl" | sha256sum -c - \
|
||||||
|
&& chmod 0755 /usr/local/bin/kubectl \
|
||||||
|
&& curl -fsSLo "/tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz" "https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz" \
|
||||||
|
&& curl -fsSLo "/tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz.sha256sum" "https://get.helm.sh/helm-v${HELM_VERSION}-linux-amd64.tar.gz.sha256sum" \
|
||||||
|
&& echo "$(awk '{print $1}' /tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz.sha256sum) /tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | sha256sum -c - \
|
||||||
|
&& tar -xzf "/tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz" -C /tmp \
|
||||||
|
&& install -m 0755 /tmp/linux-amd64/helm /usr/local/bin/helm \
|
||||||
|
&& 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 \
|
||||||
|
&& mkdir -p /etc/bash_completion.d \
|
||||||
|
&& 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 \
|
||||||
|
&& rm -f /tmp/kubectl.sha256 \
|
||||||
|
&& rm -rf /tmp/linux-amd64 \
|
||||||
|
&& rm -f \
|
||||||
|
"/tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz" \
|
||||||
|
"/tmp/helm-v${HELM_VERSION}-linux-amd64.tar.gz.sha256sum" \
|
||||||
|
/tmp/k9s \
|
||||||
|
/tmp/README.md \
|
||||||
|
/tmp/LICENSE \
|
||||||
|
/tmp/k9s_Linux_amd64.tar.gz \
|
||||||
|
&& rm -f /etc/ssh/ssh_host_*
|
||||||
|
|
||||||
|
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/profile/20-k8s-tools-completion.sh /etc/profile.d/20-k8s-tools-completion.sh
|
||||||
|
COPY docker/motd /etc/motd
|
||||||
|
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
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
||||||
|
CMD nc -z 127.0.0.1 22 || exit 1
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
|
||||||
212
README.md
Normal file
212
README.md
Normal 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.
|
||||||
20
docker-compose.yaml
Normal file
20
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
services:
|
||||||
|
k8s-mgmt-pod:
|
||||||
|
image: k8s-mgmt-pod:local
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: k8s-mgmt-pod
|
||||||
|
ports:
|
||||||
|
- "${HOST_SSH_PORT:-2222}:22"
|
||||||
|
- "${HOST_WEBSSH_PORT:-3000}:3000"
|
||||||
|
environment:
|
||||||
|
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-this-admin-password}
|
||||||
|
USER_PASSWORD: ${USER_PASSWORD:-change-this-user-password}
|
||||||
|
ADMIN_SSH_PUBKEY: ${ADMIN_SSH_PUBKEY:-}
|
||||||
|
USER_SSH_PUBKEY: ${USER_SSH_PUBKEY:-}
|
||||||
|
ENABLE_BOOTSTRAP_KUBECONFIG: ${ENABLE_BOOTSTRAP_KUBECONFIG:-true}
|
||||||
|
BOOTSTRAP_KUBECONFIG: /bootstrap/kubeconfig/config
|
||||||
|
volumes:
|
||||||
|
- ${HOME}/.kube:/bootstrap/kubeconfig:ro
|
||||||
|
restart: unless-stopped
|
||||||
33
docker/motd
Normal file
33
docker/motd
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
|
||||||
|
╔════════════════════════════════════════════════════════════════╗
|
||||||
|
║ ║
|
||||||
|
║ ██╗ ███████╗██╗ ██╗ ██╗ ██╗ █████╗ ███████╗ ║
|
||||||
|
║ ██║ ██╔════╝██║ ██╔╝ ██║ ██╔╝██╔══██╗██╔════╝ ║
|
||||||
|
║ ██║ █████╗ █████╔╝ █████╔╝ ╚█████╔╝███████╗ ║
|
||||||
|
║ ██║ ██╔══╝ ██╔═██╗ ██╔═██╗ ██╔══██╗╚════██║ ║
|
||||||
|
║ ███████╗██║ ██║ ██╗ ██║ ██╗╚█████╔╝███████║ ║
|
||||||
|
║ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚════╝ ╚══════╝ ║
|
||||||
|
║ Kubernetes Management Pod - Jump Box ║
|
||||||
|
║ ║
|
||||||
|
╠════════════════════════════════════════════════════════════════╣
|
||||||
|
║ Toolbelt ║
|
||||||
|
║ - kubectl : cluster operations and troubleshooting ║
|
||||||
|
║ - lfk : LFK kubernetes management ║
|
||||||
|
║ - k9s : interactive cluster navigation ║
|
||||||
|
║ - helm : chart install, upgrade, and rollback ║
|
||||||
|
║ - plus : kubectx, kubens, jq, yq, and more ║
|
||||||
|
║ ║
|
||||||
|
╠════════════════════════════════════════════════════════════════╣
|
||||||
|
║ Quick start ║
|
||||||
|
║ kubectl get pods -A ║
|
||||||
|
║ k9s ║
|
||||||
|
║ helm list -A ║
|
||||||
|
║ lfk --help ║
|
||||||
|
║ ║
|
||||||
|
╠════════════════════════════════════════════════════════════════╣
|
||||||
|
║ Access ║
|
||||||
|
║ web ssh : browser session backed by local sshd ║
|
||||||
|
║ users : admin (passwordless sudo) | user (no sudo) ║
|
||||||
|
║ ║
|
||||||
|
╚════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
40
docker/nginx/webssh.conf
Normal file
40
docker/nginx/webssh.conf
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 3000;
|
||||||
|
listen [::]:3000;
|
||||||
|
server_name _;
|
||||||
|
|
||||||
|
location = / {
|
||||||
|
return 302 $scheme://$http_host/ssh/host/127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /ssh {
|
||||||
|
return 302 $scheme://$http_host/ssh/host/127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location = /ssh/host {
|
||||||
|
return 302 $scheme://$http_host/ssh/host/127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~ ^/ssh/host/(?!127\.0\.0\.1(?:/|$)).+ {
|
||||||
|
return 302 $scheme://$http_host/ssh/host/127.0.0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:3001;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $http_host;
|
||||||
|
proxy_set_header X-Forwarded-Host $http_host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_read_timeout 3600;
|
||||||
|
proxy_send_timeout 3600;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
docker/profile/20-k8s-tools-completion.sh
Normal file
19
docker/profile/20-k8s-tools-completion.sh
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Enable kubectl and helm completion for interactive bash sessions.
|
||||||
|
if [ -n "${BASH_VERSION:-}" ]; then
|
||||||
|
if [ -r /usr/share/bash-completion/bash_completion ]; then
|
||||||
|
. /usr/share/bash-completion/bash_completion
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r /etc/bash_completion.d/kubectl ]; then
|
||||||
|
. /etc/bash_completion.d/kubectl
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -r /etc/bash_completion.d/helm ]; then
|
||||||
|
. /etc/bash_completion.d/helm
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
alias k=kubectl
|
||||||
|
complete -F __start_kubectl k
|
||||||
16
docker/sshd/10-lfk-base.conf
Normal file
16
docker/sshd/10-lfk-base.conf
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
Port 22
|
||||||
|
Protocol 2
|
||||||
|
PermitRootLogin no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
HostKey /var/lib/k8s-mgmt-pod/ssh_host_rsa_key
|
||||||
|
HostKey /var/lib/k8s-mgmt-pod/ssh_host_ecdsa_key
|
||||||
|
HostKey /var/lib/k8s-mgmt-pod/ssh_host_ed25519_key
|
||||||
|
AuthorizedKeysFile .ssh/authorized_keys
|
||||||
|
UsePAM yes
|
||||||
|
X11Forwarding no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
AllowAgentForwarding no
|
||||||
|
PermitEmptyPasswords no
|
||||||
|
PrintMotd no
|
||||||
|
ClientAliveInterval 300
|
||||||
|
ClientAliveCountMax 2
|
||||||
223
entrypoint.sh
Normal file
223
entrypoint.sh
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
#!/bin/bash
|
||||||
|
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"
|
||||||
|
PASSWORD_AUTH_REQUIRED=0
|
||||||
|
SSHD_PID=""
|
||||||
|
WEBSSH2_PID=""
|
||||||
|
NGINX_PID=""
|
||||||
|
BOOTSTRAP_KUBECONFIG="${BOOTSTRAP_KUBECONFIG:-/bootstrap/kubeconfig/config}"
|
||||||
|
ENABLE_BOOTSTRAP_KUBECONFIG="${ENABLE_BOOTSTRAP_KUBECONFIG:-false}"
|
||||||
|
|
||||||
|
ensure_host_keys() {
|
||||||
|
if [[ ! -f "${STATE_DIR}/ssh_host_rsa_key" ]]; then
|
||||||
|
ssh-keygen -q -t rsa -b 3072 -N '' -f "${STATE_DIR}/ssh_host_rsa_key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${STATE_DIR}/ssh_host_ecdsa_key" ]]; then
|
||||||
|
ssh-keygen -q -t ecdsa -b 256 -N '' -f "${STATE_DIR}/ssh_host_ecdsa_key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${STATE_DIR}/ssh_host_ed25519_key" ]]; then
|
||||||
|
ssh-keygen -q -t ed25519 -N '' -f "${STATE_DIR}/ssh_host_ed25519_key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
chmod 0600 "${STATE_DIR}/ssh_host_"*_key
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "${STATE_DIR}"
|
||||||
|
touch "${GENERATED_PASSWORDS_FILE}"
|
||||||
|
chmod 0600 "${GENERATED_PASSWORDS_FILE}"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
printf '[k8s-mgmt-pod] %s\n' "$*"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
local exit_code=$?
|
||||||
|
|
||||||
|
if [[ -n "${WEBSSH2_PID}" ]] && kill -0 "${WEBSSH2_PID}" 2>/dev/null; then
|
||||||
|
kill "${WEBSSH2_PID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${SSHD_PID}" ]] && kill -0 "${SSHD_PID}" 2>/dev/null; then
|
||||||
|
kill "${SSHD_PID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${NGINX_PID}" ]] && kill -0 "${NGINX_PID}" 2>/dev/null; then
|
||||||
|
kill "${NGINX_PID}" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
wait || true
|
||||||
|
exit "${exit_code}"
|
||||||
|
}
|
||||||
|
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
store_generated_password() {
|
||||||
|
local username="$1"
|
||||||
|
local password="$2"
|
||||||
|
|
||||||
|
if grep -q "^${username}=" "${GENERATED_PASSWORDS_FILE}"; then
|
||||||
|
sed -i "s|^${username}=.*|${username}=${password}|" "${GENERATED_PASSWORDS_FILE}"
|
||||||
|
else
|
||||||
|
printf '%s=%s\n' "${username}" "${password}" >> "${GENERATED_PASSWORDS_FILE}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
read_generated_password() {
|
||||||
|
local username="$1"
|
||||||
|
|
||||||
|
grep -E "^${username}=" "${GENERATED_PASSWORDS_FILE}" | tail -n 1 | cut -d '=' -f 2-
|
||||||
|
}
|
||||||
|
|
||||||
|
write_authorized_keys() {
|
||||||
|
local username="$1"
|
||||||
|
local pubkey_content="$2"
|
||||||
|
local home_dir="/home/${username}"
|
||||||
|
local ssh_dir="${home_dir}/.ssh"
|
||||||
|
local auth_keys="${ssh_dir}/authorized_keys"
|
||||||
|
|
||||||
|
mkdir -p "${ssh_dir}"
|
||||||
|
chmod 0700 "${ssh_dir}"
|
||||||
|
printf '%s\n' "${pubkey_content}" > "${auth_keys}"
|
||||||
|
chmod 0600 "${auth_keys}"
|
||||||
|
chown -R "${username}:${username}" "${ssh_dir}"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_kubeconfig_for_user() {
|
||||||
|
local username="$1"
|
||||||
|
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}"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_bootstrap_kubeconfig() {
|
||||||
|
if [[ "${ENABLE_BOOTSTRAP_KUBECONFIG}" != "true" ]]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "${BOOTSTRAP_KUBECONFIG}" ]]; then
|
||||||
|
log "Skipping kubeconfig bootstrap because ${BOOTSTRAP_KUBECONFIG} is not present."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_kubeconfig_for_user admin
|
||||||
|
install_kubeconfig_for_user user
|
||||||
|
log "Installed mounted kubeconfig for admin and user."
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_authorized_keys() {
|
||||||
|
local username="$1"
|
||||||
|
local auth_keys="/home/${username}/.ssh/authorized_keys"
|
||||||
|
|
||||||
|
rm -f "${auth_keys}"
|
||||||
|
}
|
||||||
|
|
||||||
|
set_login_password() {
|
||||||
|
local username="$1"
|
||||||
|
local password="$2"
|
||||||
|
|
||||||
|
printf '%s:%s\n' "${username}" "${password}" | chpasswd
|
||||||
|
passwd -u "${username}" >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_password() {
|
||||||
|
openssl rand -base64 24 | tr -d '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_user_auth() {
|
||||||
|
local username="$1"
|
||||||
|
local pubkey_var_name="$2"
|
||||||
|
local password_var_name="$3"
|
||||||
|
local pubkey_value="${!pubkey_var_name:-}"
|
||||||
|
local password_value="${!password_var_name:-}"
|
||||||
|
local generated_password=""
|
||||||
|
local has_pubkey=0
|
||||||
|
|
||||||
|
if [[ -n "${pubkey_value}" ]]; then
|
||||||
|
write_authorized_keys "${username}" "${pubkey_value}"
|
||||||
|
has_pubkey=1
|
||||||
|
log "Configured SSH public key authentication for ${username}."
|
||||||
|
else
|
||||||
|
remove_authorized_keys "${username}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "${password_value}" ]]; then
|
||||||
|
PASSWORD_AUTH_REQUIRED=1
|
||||||
|
set_login_password "${username}" "${password_value}"
|
||||||
|
log "Configured password authentication for ${username} from environment."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${has_pubkey}" -eq 1 ]]; then
|
||||||
|
passwd -l "${username}" >/dev/null 2>&1 || true
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
PASSWORD_AUTH_REQUIRED=1
|
||||||
|
|
||||||
|
generated_password="$(read_generated_password "${username}")"
|
||||||
|
if [[ -z "${generated_password}" ]]; then
|
||||||
|
generated_password="$(generate_password)"
|
||||||
|
store_generated_password "${username}" "${generated_password}"
|
||||||
|
log "Generated random password for ${username}: ${generated_password}"
|
||||||
|
log "Store this password securely now; it is only printed when first generated."
|
||||||
|
fi
|
||||||
|
|
||||||
|
set_login_password "${username}" "${generated_password}"
|
||||||
|
}
|
||||||
|
|
||||||
|
render_sshd_auth_config() {
|
||||||
|
local password_value="no"
|
||||||
|
local keyboard_interactive_value="no"
|
||||||
|
|
||||||
|
if [[ "${PASSWORD_AUTH_REQUIRED}" -eq 1 ]]; then
|
||||||
|
password_value="yes"
|
||||||
|
keyboard_interactive_value="yes"
|
||||||
|
export WEBSSH2_AUTH_ALLOWED="password,publickey,keyboard-interactive"
|
||||||
|
else
|
||||||
|
export WEBSSH2_AUTH_ALLOWED="publickey"
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "${SSH_DYNAMIC_CONFIG}" <<EOF
|
||||||
|
PasswordAuthentication ${password_value}
|
||||||
|
KbdInteractiveAuthentication ${keyboard_interactive_value}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_runtime() {
|
||||||
|
mkdir -p /var/run/sshd
|
||||||
|
ensure_host_keys
|
||||||
|
install_bootstrap_kubeconfig
|
||||||
|
configure_user_auth admin ADMIN_SSH_PUBKEY ADMIN_PASSWORD
|
||||||
|
configure_user_auth user USER_SSH_PUBKEY USER_PASSWORD
|
||||||
|
render_sshd_auth_config
|
||||||
|
/usr/sbin/sshd -t
|
||||||
|
}
|
||||||
|
|
||||||
|
start_services() {
|
||||||
|
/usr/sbin/sshd -D -e &
|
||||||
|
SSHD_PID=$!
|
||||||
|
|
||||||
|
NODE_ENV=production npm --prefix /opt/webssh2 start &
|
||||||
|
WEBSSH2_PID=$!
|
||||||
|
|
||||||
|
nginx -g 'daemon off;' &
|
||||||
|
NGINX_PID=$!
|
||||||
|
|
||||||
|
log "sshd started on port 22."
|
||||||
|
log "WebSSH2 started on port ${WEBSSH2_LISTEN_PORT}."
|
||||||
|
log "nginx redirect/proxy started on port ${WEB_SSH_PORT}."
|
||||||
|
|
||||||
|
wait -n "${SSHD_PID}" "${WEBSSH2_PID}" "${NGINX_PID}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Only public key material is accepted via environment variables; never log secret key data.
|
||||||
|
prepare_runtime
|
||||||
|
start_services
|
||||||
82
k8s/README.md
Normal file
82
k8s/README.md
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
# Kubernetes deployment
|
||||||
|
|
||||||
|
These manifests deploy `k8s-mgmt-pod` as a single-replica bastion deployment inside its own namespace.
|
||||||
|
|
||||||
|
## Before you apply
|
||||||
|
|
||||||
|
1. Build and publish the image.
|
||||||
|
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`.
|
||||||
|
|
||||||
|
## Create the Secret
|
||||||
|
|
||||||
|
Public-key path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
|
||||||
|
--from-literal=ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
|
||||||
|
--from-literal=USER_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
|
||||||
|
--from-literal=ADMIN_PASSWORD='' \
|
||||||
|
--from-literal=USER_PASSWORD=''
|
||||||
|
```
|
||||||
|
|
||||||
|
Password path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
|
||||||
|
--from-literal=ADMIN_SSH_PUBKEY='' \
|
||||||
|
--from-literal=USER_SSH_PUBKEY='' \
|
||||||
|
--from-literal=ADMIN_PASSWORD='change-this-admin-password' \
|
||||||
|
--from-literal=USER_PASSWORD='change-this-user-password'
|
||||||
|
```
|
||||||
|
|
||||||
|
Mixed key-plus-password path:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n k8s-mgmt-pod create secret generic k8s-mgmt-pod-auth \
|
||||||
|
--from-literal=ADMIN_SSH_PUBKEY="$(cat ~/.ssh/id_ed25519.pub)" \
|
||||||
|
--from-literal=USER_SSH_PUBKEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example" \
|
||||||
|
--from-literal=ADMIN_PASSWORD='change-this-admin-password' \
|
||||||
|
--from-literal=USER_PASSWORD='change-this-user-password'
|
||||||
|
```
|
||||||
|
|
||||||
|
With that configuration, both `admin` and `user` can authenticate with either their SSH key or their password.
|
||||||
|
|
||||||
|
Resolution order is per user:
|
||||||
|
|
||||||
|
1. If a public key is supplied, key-based login is enabled.
|
||||||
|
2. If a password is also supplied, password login is enabled too.
|
||||||
|
3. If only a public key is supplied, password login is locked for that user.
|
||||||
|
4. If neither is supplied, the container generates and logs a random password for that user.
|
||||||
|
|
||||||
|
## Apply
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl apply -k k8s/
|
||||||
|
```
|
||||||
|
|
||||||
|
If you prefer individual files, apply `namespace.yaml`, `serviceaccount.yaml`, `rbac.yaml`, `deployment.yaml`, `service.yaml`, and `networkpolicy.yaml` after creating the real Secret.
|
||||||
|
|
||||||
|
## Safe local access
|
||||||
|
|
||||||
|
Port-forward the service instead of exposing it externally by default:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl -n k8s-mgmt-pod port-forward svc/k8s-mgmt-pod 2222:22 3000:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
Then connect with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh admin@localhost -p 2222
|
||||||
|
```
|
||||||
|
|
||||||
|
Or open `http://localhost:3000/ssh` in a browser.
|
||||||
|
|
||||||
|
## RBAC note
|
||||||
|
|
||||||
|
The included Role is the most security-sensitive part of this deployment. It is namespace-scoped and avoids `cluster-admin`, but it still grants broad read/write access to common namespaced resources so `lfk` remains usable as a management tool. In particular, access to `secrets`, `pods/exec`, and workload mutation verbs may be too broad for your environment and should be reduced wherever possible.
|
||||||
|
|
||||||
|
## Bastion note
|
||||||
|
|
||||||
|
This pod is a privileged operational entry point into the cluster. Treat it like a bastion: restrict who can reach it, monitor access, rotate credentials, and avoid using it as a general-purpose workstation.
|
||||||
92
k8s/deployment.yaml
Normal file
92
k8s/deployment.yaml
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
strategy:
|
||||||
|
type: Recreate
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
spec:
|
||||||
|
serviceAccountName: k8s-mgmt-pod
|
||||||
|
automountServiceAccountToken: true
|
||||||
|
securityContext:
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: k8s-mgmt-pod
|
||||||
|
# Replace this placeholder with a pinned tag or digest in production.
|
||||||
|
image: ghcr.io/<org>/<repo>/k8s-mgmt-pod:latest
|
||||||
|
imagePullPolicy: IfNotPresent
|
||||||
|
ports:
|
||||||
|
- name: ssh
|
||||||
|
containerPort: 22
|
||||||
|
protocol: TCP
|
||||||
|
- name: webssh
|
||||||
|
containerPort: 3000
|
||||||
|
protocol: TCP
|
||||||
|
env:
|
||||||
|
- name: ADMIN_SSH_PUBKEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: k8s-mgmt-pod-auth
|
||||||
|
key: ADMIN_SSH_PUBKEY
|
||||||
|
optional: true
|
||||||
|
- name: USER_SSH_PUBKEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: k8s-mgmt-pod-auth
|
||||||
|
key: USER_SSH_PUBKEY
|
||||||
|
optional: true
|
||||||
|
- name: ADMIN_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: k8s-mgmt-pod-auth
|
||||||
|
key: ADMIN_PASSWORD
|
||||||
|
optional: true
|
||||||
|
- name: USER_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: k8s-mgmt-pod-auth
|
||||||
|
key: USER_PASSWORD
|
||||||
|
optional: true
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 256Mi
|
||||||
|
limits:
|
||||||
|
cpu: 500m
|
||||||
|
memory: 512Mi
|
||||||
|
readinessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 22
|
||||||
|
initialDelaySeconds: 5
|
||||||
|
periodSeconds: 10
|
||||||
|
livenessProbe:
|
||||||
|
tcpSocket:
|
||||||
|
port: 22
|
||||||
|
initialDelaySeconds: 15
|
||||||
|
periodSeconds: 20
|
||||||
|
volumeMounts:
|
||||||
|
- name: state
|
||||||
|
mountPath: /var/lib/k8s-mgmt-pod
|
||||||
|
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
|
||||||
|
runAsGroup: 0
|
||||||
|
runAsNonRoot: false
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: false
|
||||||
|
capabilities:
|
||||||
|
drop: ["ALL"]
|
||||||
|
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "NET_BIND_SERVICE", "SETGID", "SETUID"]
|
||||||
|
volumes:
|
||||||
|
- name: state
|
||||||
|
emptyDir: {}
|
||||||
10
k8s/kustomization.yaml
Normal file
10
k8s/kustomization.yaml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: k8s-mgmt-pod
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- serviceaccount.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- networkpolicy.yaml
|
||||||
7
k8s/namespace.yaml
Normal file
7
k8s/namespace.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
k8s-mgmt-pod-access: "false"
|
||||||
24
k8s/networkpolicy.yaml
Normal file
24
k8s/networkpolicy.yaml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
policyTypes:
|
||||||
|
- Ingress
|
||||||
|
ingress:
|
||||||
|
# Starting point only. Adjust these selectors to the namespaces or pods that should reach the bastion.
|
||||||
|
- from:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: k8s-mgmt-pod
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-mgmt-pod-access: "true"
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 22
|
||||||
|
- protocol: TCP
|
||||||
|
port: 3000
|
||||||
49
k8s/rbac.yaml
Normal file
49
k8s/rbac.yaml
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
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: [""]
|
||||||
|
resources:
|
||||||
|
- configmaps
|
||||||
|
- endpoints
|
||||||
|
- events
|
||||||
|
- persistentvolumeclaims
|
||||||
|
- pods
|
||||||
|
- pods/exec
|
||||||
|
- pods/log
|
||||||
|
- secrets
|
||||||
|
- serviceaccounts
|
||||||
|
- services
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["apps"]
|
||||||
|
resources:
|
||||||
|
- daemonsets
|
||||||
|
- deployments
|
||||||
|
- replicasets
|
||||||
|
- statefulsets
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["batch"]
|
||||||
|
resources:
|
||||||
|
- cronjobs
|
||||||
|
- jobs
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
- apiGroups: ["networking.k8s.io"]
|
||||||
|
resources:
|
||||||
|
- ingresses
|
||||||
|
- networkpolicies
|
||||||
|
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
roleRef:
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
kind: Role
|
||||||
|
name: k8s-mgmt-pod
|
||||||
15
k8s/secret.yaml.example
Normal file
15
k8s/secret.yaml.example
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
# Example only. Do not commit a real Secret object with live credentials or keys.
|
||||||
|
# Create the real secret out-of-band with kubectl, Sealed Secrets, External Secrets,
|
||||||
|
# or your preferred secret manager.
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod-auth
|
||||||
|
type: Opaque
|
||||||
|
stringData:
|
||||||
|
ADMIN_SSH_PUBKEY: |
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample admin@example
|
||||||
|
USER_SSH_PUBKEY: |
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIexample user@example
|
||||||
|
ADMIN_PASSWORD: "change-this-admin-password"
|
||||||
|
USER_PASSWORD: "change-this-user-password"
|
||||||
34
k8s/service.yaml
Normal file
34
k8s/service.yaml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
ports:
|
||||||
|
- name: ssh
|
||||||
|
port: 22
|
||||||
|
targetPort: ssh
|
||||||
|
protocol: TCP
|
||||||
|
- name: webssh
|
||||||
|
port: 3000
|
||||||
|
targetPort: webssh
|
||||||
|
protocol: TCP
|
||||||
|
|
||||||
|
# Expose externally only if you explicitly want SSH and web SSH reachable from outside the cluster.
|
||||||
|
# apiVersion: v1
|
||||||
|
# kind: Service
|
||||||
|
# metadata:
|
||||||
|
# name: k8s-mgmt-pod-external
|
||||||
|
# spec:
|
||||||
|
# type: LoadBalancer
|
||||||
|
# selector:
|
||||||
|
# app.kubernetes.io/name: k8s-mgmt-pod
|
||||||
|
# ports:
|
||||||
|
# - name: ssh
|
||||||
|
# port: 22
|
||||||
|
# targetPort: ssh
|
||||||
|
# - name: webssh
|
||||||
|
# port: 3000
|
||||||
|
# targetPort: webssh
|
||||||
4
k8s/serviceaccount.yaml
Normal file
4
k8s/serviceaccount.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: k8s-mgmt-pod
|
||||||
Loading…
Add table
Add a link
Reference in a new issue