update restricted shell to force (–no-kill) option.
Build and Deploy to K3s / test (push) Successful in 1m34s
Build and Deploy to K3s / lint (push) Successful in 58s
Build and Deploy to K3s / build-and-push (push) Successful in 2m3s
Build and Deploy to K3s / deploy (push) Successful in 8s

This commit is contained in:
jasonwitty
2026-08-24 07:33:43 -07:00
parent c540beba18
commit 15ace386e3
5 changed files with 76 additions and 21 deletions
+15 -12
View File
@@ -134,6 +134,11 @@ RUN id -u socktop &>/dev/null || useradd -m -s /bin/bash socktop && \
mkdir -p /home/socktop/.config/socktop && \ mkdir -p /home/socktop/.config/socktop && \
chown -R socktop:socktop /home/socktop chown -R socktop:socktop /home/socktop
# Unprivileged user that per-session shells run as (see docker/session-shell.sh).
# Separate from socktop so a session cannot signal the server, the agent, or
# anything else that matters — the kernel refuses cross-UID signals.
RUN useradd -m -s /usr/sbin/nologin demo
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
@@ -155,7 +160,8 @@ COPY --from=node-builder /build/node_modules ./node_modules
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
COPY docker/init-config.sh /init-config.sh COPY docker/init-config.sh /init-config.sh
COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell.sh COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell.sh
RUN chmod +x /entrypoint.sh /init-config.sh /usr/local/bin/restricted-shell.sh COPY docker/session-shell.sh /usr/local/bin/session-shell.sh
RUN chmod +x /entrypoint.sh /init-config.sh /usr/local/bin/restricted-shell.sh /usr/local/bin/session-shell.sh
# Expose ports # Expose ports
# 8082 - webterm HTTP server # 8082 - webterm HTTP server
@@ -166,18 +172,15 @@ EXPOSE 8082 3001
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8082/ || exit 1 CMD curl -f http://localhost:8082/ || exit 1
# Create a wrapper script that detects if running as root or socktop user # entrypoint.sh handles both cases itself: as root it prepares /home/demo,
RUN echo '#!/bin/bash\n\ # runs the agent as the socktop user, and keeps webterm-server as root so it
if [ "$(id -u)" -eq 0 ]; then\n\ # can drop each session to the demo user (CAP_SETUID/CAP_SETGID); as non-root
# Running as root - use init-config.sh to set up and switch to socktop\n\ # it behaves as before (single-UID, no session drop).
exec /init-config.sh "$@"\n\ RUN ln -sf /entrypoint.sh /docker-entrypoint.sh
else\n\
# Running as socktop user - directly execute entrypoint\n\
exec /entrypoint.sh "$@"\n\
fi' > /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh
# Set entrypoint to the wrapper # Set entrypoint to the wrapper
ENTRYPOINT ["/docker-entrypoint.sh"] ENTRYPOINT ["/docker-entrypoint.sh"]
# Default command - use restricted shell that only allows socktop commands # Default command - sessions enter via session-shell.sh (per-session privilege
CMD ["webterm-server", "--host", "0.0.0.0", "--port", "8082", "--command", "/usr/local/bin/restricted-shell.sh"] # drop when root) which execs the restricted shell that only allows socktop
CMD ["webterm-server", "--host", "0.0.0.0", "--port", "8082", "--command", "/usr/local/bin/session-shell.sh"]
+3
View File
@@ -36,6 +36,9 @@ services:
# Optional: Set timezone # Optional: Set timezone
- TZ=America/New_York - TZ=America/New_York
# Disable socktop's local process-kill feature in every session
- SOCKTOP_NO_KILL=1
# Optional: Logging level # Optional: Logging level
- RUST_LOG=info - RUST_LOG=info
+34 -2
View File
@@ -54,12 +54,41 @@ setup_alacritty() {
echo "Alacritty setup complete" echo "Alacritty setup complete"
} }
# Prepare the home directory for the unprivileged `demo` user that sessions
# run as (see docker/session-shell.sh). Sessions need read access to the
# socktop profiles and CA certs, which are mounted under /home/socktop — copy
# them across and rewrite the cert paths, since demo cannot traverse another
# user's mounts reliably. Root-only: without root there is no demo user split.
prepare_demo_home() {
if [ "$(id -u)" -ne 0 ]; then
return
fi
echo "Preparing /home/demo for session user..."
mkdir -p /home/demo/.config/socktop/certs /home/demo/.config/alacritty
if [ -f /home/socktop/.config/socktop/profiles.json ]; then
cp /home/socktop/.config/socktop/profiles.json /home/demo/.config/socktop/profiles.json
sed -i 's|/home/socktop/|/home/demo/|g; s|/var/lib/socktop/|/home/demo/|g' /home/demo/.config/socktop/profiles.json
fi
cp /home/socktop/.config/socktop/certs/*.pem /home/demo/.config/socktop/certs/ 2>/dev/null || true
cp /home/socktop/.config/alacritty/*.toml /home/demo/.config/alacritty/ 2>/dev/null || true
chown -R demo:demo /home/demo
chmod -R go-w /home/demo
echo " ✓ /home/demo ready"
}
# Start socktop agent # Start socktop agent
start_socktop_agent() { start_socktop_agent() {
echo "Starting socktop-agent on port 3001..." echo "Starting socktop-agent on port 3001..."
# Start socktop-agent in the background on port 3001 # Start socktop-agent in the background on port 3001. When root, drop it
/usr/bin/socktop_agent --port 3001 > /tmp/socktop-agent.log 2>&1 & # to the socktop user — it only reads /proc and system metrics, and a
# separate UID keeps it out of reach of the demo session user.
if [ "$(id -u)" -eq 0 ]; then
setpriv --reuid socktop --regid socktop --clear-groups --inh-caps -all --no-new-privs \
/usr/bin/socktop_agent --port 3001 > /tmp/socktop-agent.log 2>&1 &
else
/usr/bin/socktop_agent --port 3001 > /tmp/socktop-agent.log 2>&1 &
fi
AGENT_PID=$! AGENT_PID=$!
echo "socktop-agent started (PID: $AGENT_PID)" echo "socktop-agent started (PID: $AGENT_PID)"
@@ -85,6 +114,9 @@ main() {
# Set up Alacritty # Set up Alacritty
setup_alacritty setup_alacritty
# Home directory for the per-session demo user
prepare_demo_home
# Start socktop agent # Start socktop agent
start_socktop_agent start_socktop_agent
+4 -4
View File
@@ -12,7 +12,7 @@ CYAN='\033[0;36m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
# History file # History file
HISTFILE="/home/socktop/.socktop_history" HISTFILE="${HOME:-/tmp}/.socktop_history"
HISTSIZE=1000 HISTSIZE=1000
# Load history from file # Load history from file
@@ -135,7 +135,7 @@ main() {
# Allow socktop with validated arguments only # Allow socktop with validated arguments only
if [ "$cmd" = "$input" ]; then if [ "$cmd" = "$input" ]; then
# No arguments, use default (local profile) # No arguments, use default (local profile)
/usr/bin/socktop -P local /usr/bin/socktop --no-kill -P local
else else
# Validate and sanitize arguments to prevent command injection # Validate and sanitize arguments to prevent command injection
# Only allow: -P <profile_name> or ws://<url> # Only allow: -P <profile_name> or ws://<url>
@@ -144,11 +144,11 @@ main() {
if [[ "$args" =~ ^-P[[:space:]]+[a-zA-Z0-9_-]+$ ]]; then if [[ "$args" =~ ^-P[[:space:]]+[a-zA-Z0-9_-]+$ ]]; then
# Extract profile name and validate it # Extract profile name and validate it
profile=$(echo "$args" | sed 's/-P[[:space:]]\+//') profile=$(echo "$args" | sed 's/-P[[:space:]]\+//')
/usr/bin/socktop -P "$profile" /usr/bin/socktop --no-kill -P "$profile"
# Check for websocket URL (ws:// or wss://) # Check for websocket URL (ws:// or wss://)
elif [[ "$args" =~ ^wss?://[a-zA-Z0-9\.\:/_-]+$ ]]; then elif [[ "$args" =~ ^wss?://[a-zA-Z0-9\.\:/_-]+$ ]]; then
# Validate websocket URL format # Validate websocket URL format
/usr/bin/socktop "$args" /usr/bin/socktop --no-kill "$args"
else else
# Reject anything else as potentially dangerous # Reject anything else as potentially dangerous
echo -e "${RED}Error:${NC} Invalid arguments for socktop" echo -e "${RED}Error:${NC} Invalid arguments for socktop"
+20 -3
View File
@@ -97,7 +97,7 @@ spec:
"--port", "--port",
"8082", "8082",
"--command", "--command",
"/usr/local/bin/restricted-shell.sh", "/usr/local/bin/session-shell.sh",
] ]
ports: ports:
@@ -115,6 +115,13 @@ spec:
value: "America/New_York" value: "America/New_York"
- name: RUST_LOG - name: RUST_LOG
value: "info" value: "info"
# Disable socktop's local process-kill feature for every socktop
# launched anywhere under this pod, regardless of command line.
# The restricted shell also passes --no-kill, but this is the layer
# a visitor cannot route around (no way to unset env from the
# restricted shell).
- name: SOCKTOP_NO_KILL
value: "1"
resources: resources:
limits: limits:
@@ -158,14 +165,24 @@ spec:
- name: socktop-home - name: socktop-home
mountPath: /var/lib/socktop mountPath: /var/lib/socktop
# webterm-server runs as in-container root holding ONLY
# CAP_SETUID/CAP_SETGID (everything else dropped, no privilege
# escalation), so it can drop each websocket session to the
# unprivileged `demo` user via session-shell.sh. The kernel then
# refuses any signal a session aims at the server, the agent
# (running as `socktop`), or another session's UID — the kill
# feature's UI gating stops being the only line of defense.
securityContext: securityContext:
allowPrivilegeEscalation: false allowPrivilegeEscalation: false
capabilities: capabilities:
drop: drop:
- ALL - ALL
add:
- SETUID
- SETGID
readOnlyRootFilesystem: false readOnlyRootFilesystem: false
runAsUser: 100 runAsUser: 0
runAsGroup: 101 runAsGroup: 0
volumes: volumes:
- name: config - name: config