Name update

This commit is contained in:
Miles Ward
2026-04-29 02:47:00 -04:00
commit 3517c67831
66 changed files with 9016 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
target
data/isos
data/work
.git
.github
docs
*.md
.claude
+97
View File
@@ -0,0 +1,97 @@
# syntax=docker/dockerfile:1.7
#
# PXEForge — multi-stage build.
#
# Design:
# - stage `fetch`: runs scripts/fetch-ipxe.sh to pull official iPXE binaries
# into assets/ipxe/ so the rust build can embed them via rust-embed.
# - stage `build`: compiles the workspace with cargo in release mode.
# - stage `runtime`: Debian slim image with setcap for NET_BIND_SERVICE,
# running as a non-root UID. No shell in PATH for the service user;
# attacker surface is just the pxeforge binary + libc.
#
# Why not distroless? We want setcap support and easy debug (`oc rsh`).
# Debian slim at ~75 MB + binary ~25 MB is fine for a PXE server that
# spends most of its life idle.
ARG RUST_VERSION=1.82
########## fetch iPXE binaries ##########
FROM debian:12-slim AS fetch
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh
########## build pxeforge ##########
FROM rust:${RUST_VERSION}-bookworm AS build
WORKDIR /src
# Copy the whole workspace in one go. We used to do a two-pass "cache-prime
# with stubs, then real build" dance for dep-compile reuse; that turned out
# to silently serve stale stub binaries when cargo's fingerprint didn't
# notice the source swap. A single build is ~1.5 min longer on cold cache
# but guarantees the binary reflects the sources we copied.
COPY Cargo.toml rust-toolchain.toml ./
COPY crates/ crates/
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe
# Cache cargo registry + target across builds. The `--no-edit` touch is
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
# networked FS; this forces a fingerprint check.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target,sharing=locked \
find crates -name '*.rs' -exec touch {} + && \
cargo build --release --bin pxeforge && \
cp target/release/pxeforge /pxeforge && \
ls -l /pxeforge
########## runtime ##########
FROM debian:12-slim AS runtime
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ca-certificates libcap2-bin tini gosu iproute2 \
wimtools samba nfs-common \
&& rm -rf /var/lib/apt/lists/* \
&& useradd --system --uid 10001 --home-dir /var/lib/pxeforge --shell /usr/sbin/nologin pxeforge \
&& mkdir -p /var/lib/pxeforge/isos /var/lib/pxeforge/work /var/lib/pxeforge/smb \
&& chown -R pxeforge:pxeforge /var/lib/pxeforge
# Runtime deps explained:
# wimtools - provides `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
# samba - `smbd` serves extracted Windows install media on :445 for WinPE
# to `net use`. Guest read-only, scoped to /var/lib/pxeforge/smb.
# nfs-common - provides `mount.nfs` / `mount.nfs4` for the Storage tab's
# NFS share manager. Mount also requires the container to run
# with CAP_SYS_ADMIN — without it, mount(2) returns EPERM and
# the manager surfaces a clear error in the UI instead of
# failing silently.
# iproute2 - `ip addr` / `ip route` for the auto-detected Network tab
# fields (NIC name, subnet mask, default gateway). Tiny,
# always available; we don't pull in netlink crates for
# this one-shot startup probe.
# gosu - drops privileges cleanly from root after the entrypoint fixes
# bind-mount ownership (common OpenShift/Docker UX issue).
# Windows-specific tools only activate when the WebUI toggle is on.
COPY --from=build /pxeforge /usr/local/bin/pxeforge
COPY deploy/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
# Grant the binary the ability to bind <1024 ports as a non-root user.
# This is the only capability PXEForge needs for proxy-mode DHCP + TFTP + HTTP.
RUN setcap cap_net_bind_service=+ep /usr/local/bin/pxeforge
# IMPORTANT: we do NOT `USER pxeforge` here. The entrypoint runs as root,
# chowns the mounted data dirs, then execs the binary via gosu as pxeforge.
# OpenShift ignores USER directives anyway (it injects its own uid), and
# there entrypoint.sh's non-root branch just execs directly.
WORKDIR /var/lib/pxeforge
ENV PXEFORGE_ISO_DIR=/var/lib/pxeforge/isos \
PXEFORGE_WORK_DIR=/var/lib/pxeforge/work \
PXEFORGE_LOG=info,pxeforge=info
EXPOSE 67/udp 69/udp 4011/udp 80/tcp 445/tcp
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
+32
View File
@@ -0,0 +1,32 @@
#!/bin/sh
# Container entrypoint that handles the common bind-mount-as-root case.
#
# When volumes are bind-mounted into the container (e.g. `-v ./data/isos:...`),
# they come up owned by the host uid:gid — often root:root. The pxeforge
# binary runs as uid 10001 and can't write there. This script, when started
# as root, chowns the two state dirs to the pxeforge user, then drops
# privileges via gosu before execing the binary.
#
# If the container is already running as non-root (OpenShift does this via
# its own uid assignment from the SCC), we skip the chown attempt and just
# exec the binary — OpenShift either preconfigures the volume with fsGroup
# or the operator is on their own for permissions.
set -e
PXEFORGE_UID=${PXEFORGE_UID:-10001}
PXEFORGE_GID=${PXEFORGE_GID:-10001}
DATA_DIRS="/var/lib/pxeforge/isos /var/lib/pxeforge/work /var/lib/pxeforge/smb"
if [ "$(id -u)" = "0" ]; then
for d in $DATA_DIRS; do
if [ -d "$d" ]; then
chown -R "${PXEFORGE_UID}:${PXEFORGE_GID}" "$d" 2>/dev/null || true
fi
done
# Re-exec ourselves under the pxeforge user so the binary inherits a
# clean process environment and a predictable umask.
exec gosu "${PXEFORGE_UID}:${PXEFORGE_GID}" /usr/local/bin/pxeforge "$@"
fi
# Non-root: straight exec, no chown attempt.
exec /usr/local/bin/pxeforge "$@"
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: Namespace
metadata:
name: pxeforge
labels:
# Allow privileged pods (host-network) in this namespace only. The pod
# itself still runs non-root with only NET_BIND_SERVICE — privileged
# here is about namespace pod-security, not container privileges.
pod-security.kubernetes.io/enforce: privileged
pod-security.kubernetes.io/warn: privileged
pod-security.kubernetes.io/audit: privileged
+80
View File
@@ -0,0 +1,80 @@
---
# Custom SCC for PXEForge.
#
# The default `restricted-v2` SCC blocks host network and all capabilities,
# which PXE cannot tolerate: DHCPDISCOVER is an L2 broadcast that CNI overlays
# do not deliver into pod netns. We grant the minimum set needed:
#
# - allowHostNetwork: true — required to receive broadcast DHCP
# - allowHostPorts: true — exposes 67/69/4011/80 on the node
# - requiredDropCapabilities strips the usual dangerous caps
# - allowedCapabilities:
# NET_BIND_SERVICE — bind <1024 as non-root
# - runAsUser.type: MustRunAsRange — force non-root uid mapped via setcap
# - readOnlyRootFilesystem: true — binary is in / (set by image), data
# dirs are mounted elsewhere
#
# We do NOT grant NET_RAW / NET_ADMIN / SYS_ADMIN. Proxy-mode DHCP does not
# need raw sockets (see architecture memory).
apiVersion: security.openshift.io/v1
kind: SecurityContextConstraints
metadata:
name: pxeforge-scc
annotations:
kubernetes.io/description: >-
Minimal SCC for PXEForge: host network + NET_BIND_SERVICE only, no raw
sockets, no privileged mode.
allowPrivilegedContainer: false
allowPrivilegeEscalation: false
allowHostNetwork: true
allowHostPorts: true
allowHostPID: false
allowHostIPC: false
allowedCapabilities:
- NET_BIND_SERVICE
requiredDropCapabilities:
- ALL
defaultAddCapabilities: []
readOnlyRootFilesystem: true
runAsUser:
type: MustRunAsRange
seLinuxContext:
type: MustRunAs
fsGroup:
type: MustRunAs
supplementalGroups:
type: RunAsAny
volumes:
- configMap
- downwardAPI
- emptyDir
- persistentVolumeClaim
- projected
- secret
users: []
groups: []
---
# Bind the SCC to the pxeforge service account.
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: pxeforge-scc-use
rules:
- apiGroups: ["security.openshift.io"]
resources: ["securitycontextconstraints"]
resourceNames: ["pxeforge-scc"]
verbs: ["use"]
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: pxeforge-scc-use
namespace: pxeforge
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: pxeforge-scc-use
subjects:
- kind: ServiceAccount
name: pxeforge
namespace: pxeforge
+35
View File
@@ -0,0 +1,35 @@
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: pxeforge
namespace: pxeforge
---
apiVersion: v1
kind: ConfigMap
metadata:
name: pxeforge-config
namespace: pxeforge
data:
# Toggle DHCP proxy on or off. "proxy" = answer PXE clients alongside an
# existing DHCP server. "disabled" = require operator to point an external
# DHCP at us via next-server/filename.
PXEFORGE_DHCP_MODE: "proxy"
# Override if auto-detection picks the wrong NIC in multi-homed pods.
# Leave unset to auto-detect from the node's primary IPv4.
# PXEFORGE_PUBLIC_IP: "10.0.0.5"
PXEFORGE_LOG: "info,pxeforge=info"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pxeforge-isos
namespace: pxeforge
spec:
# ReadWriteOnce is fine — we deploy as a single replica since DHCP proxy
# coordination across replicas is not useful (clients hit whichever node
# hostNetwork catches their broadcast).
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 200Gi
+104
View File
@@ -0,0 +1,104 @@
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: pxeforge
namespace: pxeforge
labels:
app.kubernetes.io/name: pxeforge
spec:
# Single replica by design (see PVC comment). If HA is needed later, split
# the HTTP/web plane (scalable, stateless) from the DHCP-proxy/TFTP plane
# (anycast / per-node daemonset).
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: pxeforge
template:
metadata:
labels:
app.kubernetes.io/name: pxeforge
spec:
serviceAccountName: pxeforge
# L2 broadcast (DHCPDISCOVER) does not cross most CNI overlays into
# pod netns. Host network is the working path.
hostNetwork: true
dnsPolicy: ClusterFirstWithHostNet
securityContext:
# setcap on the binary allows non-root <1024 binding. No need to
# run as root.
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
containers:
- name: pxeforge
image: ghcr.io/casperadmin/pxeforge:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: dhcp
containerPort: 67
hostPort: 67
protocol: UDP
- name: tftp
containerPort: 69
hostPort: 69
protocol: UDP
- name: pxe
containerPort: 4011
hostPort: 4011
protocol: UDP
- name: http
containerPort: 80
hostPort: 80
protocol: TCP
- name: smb
containerPort: 445
hostPort: 445
protocol: TCP
envFrom:
- configMapRef:
name: pxeforge-config
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
capabilities:
drop: ["ALL"]
add: ["NET_BIND_SERVICE"]
volumeMounts:
- name: isos
mountPath: /var/lib/pxeforge/isos
- name: work
mountPath: /var/lib/pxeforge/work
- name: tmp
mountPath: /tmp
readinessProbe:
httpGet:
path: /api/status
port: 80
initialDelaySeconds: 3
periodSeconds: 5
livenessProbe:
httpGet:
path: /api/status
port: 80
initialDelaySeconds: 15
periodSeconds: 15
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
volumes:
- name: isos
persistentVolumeClaim:
claimName: pxeforge-isos
- name: work
emptyDir: {}
- name: tmp
emptyDir: {}
+43
View File
@@ -0,0 +1,43 @@
---
# Service for the web UI / API. Using host network means the pod IP is the
# node IP, so this Service is mostly useful for cluster-internal ingress to
# the management UI via Route below.
apiVersion: v1
kind: Service
metadata:
name: pxeforge
namespace: pxeforge
labels:
app.kubernetes.io/name: pxeforge
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: pxeforge
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
- name: smb
port: 445
targetPort: 445
protocol: TCP
---
# Expose the web UI through an OpenShift Route. Clients on the PXE network
# still talk to the node directly on UDP 67/69/4011 — the Route only covers
# the TCP/80 management plane.
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: pxeforge
namespace: pxeforge
spec:
to:
kind: Service
name: pxeforge
weight: 100
port:
targetPort: http
tls:
termination: edge
insecureEdgeTerminationPolicy: Redirect