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