diff --git a/README.md b/README.md index e831489..b2e4dc8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/openshift/00-namespace.yaml b/openshift/00-namespace.yaml new file mode 100644 index 0000000..b72c6f9 --- /dev/null +++ b/openshift/00-namespace.yaml @@ -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) diff --git a/openshift/01-gitea-source-secret.yaml b/openshift/01-gitea-source-secret.yaml new file mode 100644 index 0000000..222a22e --- /dev/null +++ b/openshift/01-gitea-source-secret.yaml @@ -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= +# +# 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 diff --git a/openshift/02-vshell-imagestream.yaml b/openshift/02-vshell-imagestream.yaml new file mode 100644 index 0000000..9e484dc --- /dev/null +++ b/openshift/02-vshell-imagestream.yaml @@ -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 diff --git a/openshift/03-buildconfig.yaml b/openshift/03-buildconfig.yaml new file mode 100644 index 0000000..ea823a3 --- /dev/null +++ b/openshift/03-buildconfig.yaml @@ -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 diff --git a/openshift/04-base-imagestream.yaml b/openshift/04-base-imagestream.yaml new file mode 100644 index 0000000..3427595 --- /dev/null +++ b/openshift/04-base-imagestream.yaml @@ -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 diff --git a/openshift/05-dockerhub-pull-secret.yaml b/openshift/05-dockerhub-pull-secret.yaml new file mode 100644 index 0000000..b3e67bb --- /dev/null +++ b/openshift/05-dockerhub-pull-secret.yaml @@ -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= \ +# --docker-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 -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"}}} diff --git a/openshift/README.md b/openshift/README.md new file mode 100644 index 0000000..9dcf36d --- /dev/null +++ b/openshift/README.md @@ -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= + +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. diff --git a/openshift/runtime/00-serviceaccount.yaml b/openshift/runtime/00-serviceaccount.yaml new file mode 100644 index 0000000..3095c6a --- /dev/null +++ b/openshift/runtime/00-serviceaccount.yaml @@ -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 diff --git a/openshift/runtime/01-scc.yaml b/openshift/runtime/01-scc.yaml new file mode 100644 index 0000000..c1c5b1a --- /dev/null +++ b/openshift/runtime/01-scc.yaml @@ -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 diff --git a/openshift/runtime/02-pvc.yaml b/openshift/runtime/02-pvc.yaml new file mode 100644 index 0000000..35a59a5 --- /dev/null +++ b/openshift/runtime/02-pvc.yaml @@ -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 diff --git a/openshift/runtime/03-deployment.yaml b/openshift/runtime/03-deployment.yaml new file mode 100644 index 0000000..6d74d2b --- /dev/null +++ b/openshift/runtime/03-deployment.yaml @@ -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 diff --git a/openshift/runtime/04-service.yaml b/openshift/runtime/04-service.yaml new file mode 100644 index 0000000..8580c7c --- /dev/null +++ b/openshift/runtime/04-service.yaml @@ -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 diff --git a/openshift/runtime/05-route.yaml b/openshift/runtime/05-route.yaml new file mode 100644 index 0000000..6064c8f --- /dev/null +++ b/openshift/runtime/05-route.yaml @@ -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://: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