Add OpenShift manifests for in-cluster build and runtime

- build chain: project, Gitea basic-auth source secret, output ImageStream,
  BuildConfig (Docker strategy, ubuntu:24.04 base), optional base-image
  ImageStream and Docker Hub pull secret
- runtime: ServiceAccount + least-privilege SCC and its RoleBinding (VShell
  needs UID 0), PVC for config/certificate, Deployment with ImageStream
  trigger, ClusterIP Service, passthrough HTTPS Route
- all manifests verified with 'oc create --dry-run=client' against the
  OpenShift 4.22 client scheme (12/12 decode cleanly)
This commit is contained in:
mward4 committed 2026-10-05 13:25:10 -04:00
1 parent a6f8e67976
commit 9dbe3749ba
14 files changed
+503

No files matched your search

+7
View File
@@ -96,6 +96,13 @@ user-namespace chroot on high ports and probes every listener from outside:
SSH banner, FTPS banner, HTTPS/TLS handshake, implicit FTPS handshake and the
HTTP→HTTPS redirect, after checking `-version`, `-test_config` and `-license`.
## OpenShift
`openshift/` holds drop-in manifests that build this image in-cluster
(`BuildConfig` + `ImageStream` + Gitea source secret) and, optionally, run it
(custom SCC, PVC, Deployment, Service, passthrough Route). See
[openshift/README.md](openshift/README.md) for the apply order.
## Licensing
VShell is commercial, closed-source software distributed under the VanDyke
+10
View File
@@ -0,0 +1,10 @@
# Project for the VShell image build and (optional) runtime objects.
apiVersion: v1
kind: Namespace
metadata:
name: vshell
labels:
app.kubernetes.io/name: vshell
annotations:
openshift.io/display-name: VShell
openshift.io/description: VShell 5.0.1-3985 container image build (VanDyke SSH2/SFTP/FTPS/HTTPS server)
+23
View File
@@ -0,0 +1,23 @@
# Git credentials for the private Gitea repository that holds the Dockerfile,
# the build tooling and the vendored .deb.
#
# The password is the Gitea access token (see ~/.dsh/AGENTS.md on the bastion).
# Do not commit a real token here - create the secret from the command line:
#
# oc create secret generic gitea-vshell-source -n vshell \
# --type=kubernetes.io/basic-auth \
# --from-literal=username=mward4 \
# --from-literal=password=<gitea-token>
#
# This file is the equivalent manifest if you prefer to apply it directly.
apiVersion: v1
kind: Secret
metadata:
name: gitea-vshell-source
namespace: vshell
labels:
app.kubernetes.io/name: vshell
type: kubernetes.io/basic-auth
stringData:
username: mward4
password: REPLACE_WITH_GITEA_TOKEN
+18
View File
@@ -0,0 +1,18 @@
# Output image stream for the built image. The build pushes vshell:latest here
# and the runtime Deployment pulls from it.
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: vshell
namespace: vshell
labels:
app.kubernetes.io/name: vshell
spec:
lookupPolicy:
local: true
tags:
- name: latest
importPolicy:
scheduled: false
referencePolicy:
type: Local
+49
View File
@@ -0,0 +1,49 @@
# In-cluster build of the VShell image.
#
# The repository already vendors the vendor .deb, so the only things the build
# pod fetches from the network are:
# - gitea.milesward.dev (this git repo, authenticated by gitea-vshell-source)
# - registry-1.docker.io (base image ubuntu:24.04)
# - archive.ubuntu.com (apt-get install openssl in the Dockerfile)
# On an air-gapped cluster, mirror ubuntu:24.04 and openssl into an internal
# registry first; see 04-base-imagestream.yaml and openshift/README.md.
apiVersion: build.openshift.io/v1
kind: BuildConfig
metadata:
name: vshell
namespace: vshell
labels:
app.kubernetes.io/name: vshell
spec:
runPolicy: Serial
source:
type: Git
git:
uri: https://gitea.milesward.dev/mward4/vshell.git
ref: main
sourceSecret:
name: gitea-vshell-source
strategy:
type: Docker
dockerStrategy:
dockerfilePath: Dockerfile
from:
kind: DockerImage
name: docker.io/library/ubuntu:24.04
# Pin the base by digest instead of tag when you need reproducible builds:
# name: docker.io/library/ubuntu@sha256:a60ef8ec184edb5ed91f5983f00261359fb2fe20f603e8d488b9477d32fcad70
output:
to:
kind: ImageStreamTag
name: vshell:latest
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 4Gi
successfulBuildsHistoryLimit: 5
failedBuildsHistoryLimit: 3
triggers:
- type: ConfigChange
+37
View File
@@ -0,0 +1,37 @@
# Optional: keep the Ubuntu base image inside the cluster instead of pulling it
# from Docker Hub on every build (rate limits, air-gapped clusters, or an
# ImageDigestMirrorSet that rewrites docker.io).
#
# Import the tag once:
# oc import-image ubuntu:24.04 -n vshell \
# --from=docker.io/library/ubuntu:24.04 --confirm
#
# Then point the build at it in 03-buildconfig.yaml:
# strategy:
# dockerStrategy:
# from:
# kind: ImageStreamTag
# name: ubuntu:24.04
#
# Equivalent one-shot without this file:
# oc tag docker.io/library/ubuntu:24.04 vshell/ubuntu:24.04 --scheduled
apiVersion: image.openshift.io/v1
kind: ImageStream
metadata:
name: ubuntu
namespace: vshell
labels:
app.kubernetes.io/name: vshell
app.kubernetes.io/component: base-image
spec:
lookupPolicy:
local: true
tags:
- name: "24.04"
from:
kind: DockerImage
name: docker.io/library/ubuntu:24.04
importPolicy:
scheduled: false
referencePolicy:
type: Local
+28
View File
@@ -0,0 +1,28 @@
# Optional: Docker Hub credentials for the build's base-image pull.
# Anonymous pulls of docker.io/library/ubuntu are rate limited and builds fail
# with "toomanyrequests" on a busy cluster.
#
# Create it with your own credentials (do not commit them):
#
# oc create secret docker-registry dockerhub-pull -n vshell \
# --docker-server=docker.io \
# --docker-username=<dockerhub-user> \
# --docker-password=<dockerhub-token-or-password>
#
# # let build pods use it for pulls
# oc secrets link builder dockerhub-pull -n vshell --for=pull
#
# If the cluster already has a global pull secret, linking it is enough:
# oc secrets link builder <global-pull-secret> -n vshell --for=pull
apiVersion: v1
kind: Secret
metadata:
name: dockerhub-pull
namespace: vshell
labels:
app.kubernetes.io/name: vshell
app.kubernetes.io/component: base-image
type: kubernetes.io/dockerconfigjson
stringData:
.dockerconfigjson: |
{"auths":{"https://index.docker.io/v1/":{"username":"REPLACE","password":"REPLACE","auth":"REPLACE_BASE64_USER_COLON_PASSWORD"}}}
+83
View File
@@ -0,0 +1,83 @@
# OpenShift manifests for VShell
Everything needed to build `vshell:latest` inside an OpenShift project and, if you
want, to run it there. Write-tested against the OpenShift 4.22 client scheme and
matching the 4.21 cluster described in `/workspace/openshift-win11/FINDINGS.md`.
## Build (required)
| File | What it does |
|---|---|
| [`00-namespace.yaml`](00-namespace.yaml) | Project `vshell`. |
| [`01-gitea-source-secret.yaml`](01-gitea-source-secret.yaml) | `kubernetes.io/basic-auth` secret for the private Gitea repo. |
| [`02-vshell-imagestream.yaml`](02-vshell-imagestream.yaml) | Output `ImageStream/vshell` (`latest`). |
| [`03-buildconfig.yaml`](03-buildconfig.yaml) | `BuildConfig/vshell` — Docker strategy, git source, `ubuntu:24.04` base. |
| [`04-base-imagestream.yaml`](04-base-imagestream.yaml) | Optional: keep the Ubuntu base inside the cluster instead of pulling Docker Hub on every build. |
| [`05-dockerhub-pull-secret.yaml`](05-dockerhub-pull-secret.yaml) | Optional: Docker Hub credentials so base pulls are not rate limited. |
```sh
oc apply -f 00-namespace.yaml
# password = the Gitea token from ~/.dsh/AGENTS.md (do not commit it into 01-*.yaml)
oc create secret generic gitea-vshell-source -n vshell \
--type=kubernetes.io/basic-auth \
--from-literal=username=mward4 \
--from-literal=password=<gitea-token>
oc apply -f 02-vshell-imagestream.yaml
oc apply -f 03-buildconfig.yaml
oc start-build vshell -n vshell --follow
oc get is vshell -n vshell
```
The repository vendors the vendor `.deb`, so the build pod only needs egress to
`gitea.milesward.dev`, `registry-1.docker.io` and `archive.ubuntu.com`
(`apt-get install openssl`). Air-gapped clusters: import `ubuntu:24.04` with
`04-base-imagestream.yaml`, point `strategy.dockerStrategy.from` at
`ImageStreamTag ubuntu:24.04`, and add an internal apt mirror.
`replicas: 1` is not a build concern but a runtime one: the daemons own fixed
ports and a single host key, so never scale the Deployment.
## Runtime (optional)
| File | What it does |
|---|---|
| [`runtime/00-serviceaccount.yaml`](runtime/00-serviceaccount.yaml) | `ServiceAccount/vshell`. |
| [`runtime/01-scc.yaml`](runtime/01-scc.yaml) | Least-privilege SCC that allows UID 0 + Role/RoleBinding so only this SA may use it. |
| [`runtime/02-pvc.yaml`](runtime/02-pvc.yaml) | `PersistentVolumeClaim/vshell-data` for config + TLS certificate. |
| [`runtime/03-deployment.yaml`](runtime/03-deployment.yaml) | Deployment, auto-updated by the `image.openshift.io/triggers` annotation. |
| [`runtime/04-service.yaml`](runtime/04-service.yaml) | ClusterIP service for ports 22/21/990/80/443. |
| [`runtime/05-route.yaml`](runtime/05-route.yaml) | HTTPS Route, **passthrough** termination. |
```sh
oc apply -f runtime/
oc get route vshell-https -n vshell
```
`restricted-v2` forbids UID 0, so the SCC (or `oc adm policy add-scc-to-user anyuid -z vshell -n vshell`)
is required — VShell reads `/etc/shadow` through PAM, writes `/var/run/*.pid`
and creates root-owned host keys. The custom SCC grants no extra capabilities
and no host access.
The Route is passthrough because `vshell-httpsd` terminates TLS itself with the
PKCS#12 certificate the entrypoint generated; clients therefore see a
self-signed certificate until you mount a real one at `$VSHELL_TLS_P12`. SSH
(22), explicit FTPS (21) and implicit FTPS (990) cannot go through a Route —
expose the Service with a NodePort/LoadBalancer or an external load balancer.
The Deployment points `VSHELL_CONFIG` and `VSHELL_TLS_P12` at the PVC-mounted
`/data`, which keeps the vendor defaults and the `HttpsFiles/` web assets
visible. The SSH host key stays at `/usr/local/etc/vshelld_hostkey` unless you
set `HostKeyFilenames /data/vshelld_hostkey` in the generated config.
## Caveats
- Builds run as root inside the build pod by default, which the Dockerfile's
`apt-get` and `dpkg -i` need. Unprivileged builds (`BUILD_PRIVILEGED=false`)
are an opt-in Developer Preview feature and would break this Dockerfile.
- VShell runs under its 60-day evaluation license (expires 2026-12-04) until a
license file is mounted and `LicenseFilePath` is set.
- The image and repository are private because VShell is commercial software
distributed under the VanDyke Software EULA.
+10
View File
@@ -0,0 +1,10 @@
# Service account the VShell pod runs as. It is bound to the custom SCC in
# 01-scc.yaml, because VShell must run as root (PAM reads /etc/shadow, the
# daemons write /var/run/*.pid and generate host keys owned by root).
apiVersion: v1
kind: ServiceAccount
metadata:
name: vshell
namespace: vshell
labels:
app.kubernetes.io/name: vshell
+73
View File
@@ -0,0 +1,73 @@
# OpenShift runs pods under restricted-v2 by default, which forbids UID 0.
# VShell needs root, so grant a narrow, dedicated SCC instead of anyuid.
#
# Quick alternative (broader, uses the stock anyuid SCC):
# oc adm policy add-scc-to-user anyuid -z vshell -n vshell
#
# This SCC is the least-privilege version: no privileged containers, no host
# namespaces, no hostPath, no extra capabilities, but RunAsAny so the image's
# root user is allowed.
apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: vshell-root
labels:
app.kubernetes.io/name: vshell
allowHostDirVolumePlugin: false
allowHostIPC: false
allowHostNetwork: false
allowHostPID: false
allowHostPorts: false
allowPrivilegedContainer: false
allowPrivilegeEscalation: true
allowedCapabilities: []
defaultAddCapabilities: []
requiredDropCapabilities: []
readOnlyRootFilesystem: false
runAsUser:
type: RunAsAny
seLinuxContext:
type: MustRunAs
fsGroup:
type: RunAsAny
supplementalGroups:
type: RunAsAny
seccompProfiles:
- runtime/default
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- projected
- secret
---
# Namespaced RBAC so only the vshell service account may use that SCC.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: vshell-scc-use
namespace: vshell
rules:
- apiGroups:
- security.openshift.io
resources:
- securitycontextconstraints
resourceNames:
- vshell-root
verbs:
- use
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: vshell-scc-use
namespace: vshell
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: vshell-scc-use
subjects:
- kind: ServiceAccount
name: vshell
namespace: vshell
+20
View File
@@ -0,0 +1,20 @@
# Persistence for the generated server configuration and the self-signed TLS
# certificate. The entrypoint is pointed at this volume with VSHELL_CONFIG and
# VSHELL_TLS_P12 in the Deployment, so the vendor defaults and the HttpsFiles/
# web assets under /usr/local/etc stay visible.
#
# The SSH host key path is not redirected here; set HostKeyFilenames in
# vshelld_config to /data/vshelld_hostkey if you want it to survive restarts too.
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vshell-data
namespace: vshell
labels:
app.kubernetes.io/name: vshell
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
+81
View File
@@ -0,0 +1,81 @@
# VShell runtime. Single replica: the daemons own fixed ports and one host key,
# so they are not horizontally scalable.
#
# The image tag is kept in sync by the image.openshift.io/triggers annotation,
# which fires whenever a new vshell:latest build finishes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: vshell
namespace: vshell
labels:
app.kubernetes.io/name: vshell
annotations:
image.openshift.io/triggers: |
[{"from":{"kind":"ImageStreamTag","name":"vshell:latest"},"fieldPath":"spec.template.spec.containers[0].image"}]
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: vshell
template:
metadata:
labels:
app.kubernetes.io/name: vshell
spec:
serviceAccountName: vshell
securityContext:
runAsUser: 0
runAsGroup: 0
fsGroup: 0
containers:
- name: vshell
image: vshell:latest
imagePullPolicy: Always
env:
- name: VSHELL_CONFIG
value: /data/vshelld_config
- name: VSHELL_TLS_P12
value: /data/vshell-https.p12
ports:
- name: ssh
containerPort: 22
protocol: TCP
- name: ftps
containerPort: 21
protocol: TCP
- name: ftps-implicit
containerPort: 990
protocol: TCP
- name: http
containerPort: 80
protocol: TCP
- name: https
containerPort: 443
protocol: TCP
volumeMounts:
- name: data
mountPath: /data
readinessProbe:
tcpSocket:
port: ssh
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
tcpSocket:
port: ssh
initialDelaySeconds: 30
periodSeconds: 20
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 2Gi
volumes:
- name: data
persistentVolumeClaim:
claimName: vshell-data
+37
View File
@@ -0,0 +1,37 @@
# Cluster-internal entry point for all four VShell listeners.
#
# Only 80/443 can be exposed through an OpenShift Route. SSH (22), explicit FTPS
# (21) and implicit FTPS (990) need a NodePort/LoadBalancer service or an
# external load balancer in front of this ClusterIP service.
apiVersion: v1
kind: Service
metadata:
name: vshell
namespace: vshell
labels:
app.kubernetes.io/name: vshell
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: vshell
ports:
- name: ssh
port: 22
targetPort: ssh
protocol: TCP
- name: ftps
port: 21
targetPort: ftps
protocol: TCP
- name: ftps-implicit
port: 990
targetPort: ftps-implicit
protocol: TCP
- name: http
port: 80
targetPort: http
protocol: TCP
- name: https
port: 443
targetPort: https
protocol: TCP
+27
View File
@@ -0,0 +1,27 @@
# HTTPS access through the OpenShift router.
#
# Termination is passthrough: vshell-httpsd does its own TLS with the PKCS#12
# certificate the entrypoint generated, so the router must not decrypt. Clients
# therefore see the container's self-signed certificate unless you mount a real
# certificate at $VSHELL_TLS_P12.
#
# HTTP is not routed: VShell's port-80 listener only redirects to https://<host>:443
# and the redirect target uses the container's own hostname.
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: vshell-https
namespace: vshell
labels:
app.kubernetes.io/name: vshell
spec:
to:
kind: Service
name: vshell
weight: 100
port:
targetPort: https
tls:
termination: passthrough
insecureEdgeTerminationPolicy: None
wildcardPolicy: None