Compare commits

..

No commits in common. "9fb9d9ab500f34245c0b86191b2aa0506e7aac76" and "e2249897029b3f48f694f91dcc652d817290b28e" have entirely different histories.

7 changed files with 93 additions and 380 deletions

1
.gitignore vendored
View File

@ -38,4 +38,3 @@ scripts/publish-to-gitea-multiarch.sh
scripts/publish-to-gitea.sh scripts/publish-to-gitea.sh
scripts/verify_upgrade.sh scripts/verify_upgrade.sh
scripts/check-setup.sh scripts/check-setup.sh
scripts/test-docker-config.sh

View File

@ -1,130 +1,127 @@
# Multi-stage Dockerfile for socktop webterm # Dockerfile for socktop webterm
# This reduces the final image size significantly by separating build and runtime # Based on Debian Trixie Slim with all required dependencies
# ============================================================================
# Stage 1: Rust Builder
# ============================================================================
FROM rust:1.90-slim-bookworm AS rust-builder
WORKDIR /build
# Install build dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
pkg-config \
libssl-dev \
&& rm -rf /var/lib/apt/lists/*
# Copy only dependency files first for better caching
COPY Cargo.toml Cargo.lock ./
# Create dummy source to cache dependencies
RUN mkdir src && \
echo "fn main() {}" > src/server.rs && \
echo "pub fn lib() {}" > src/lib.rs && \
cargo build --release && \
rm -rf src target/release/webterm-server target/release/deps/webterm*
# Copy actual source code
COPY src ./src
COPY templates ./templates
COPY static ./static
# Build the actual application (force rebuild by touching sources)
RUN touch src/server.rs src/lib.rs && \
cargo build --release && \
strip target/release/webterm-server
# ============================================================================
# Stage 2: Node.js Builder
# ============================================================================
FROM node:20-slim AS node-builder
WORKDIR /build
# Copy package files
COPY package.json package-lock.json ./
COPY static ./static
# Install only production dependencies
RUN npm ci --only=production && \
# Copy static files to node_modules for serving
cp static/terminado-addon.js node_modules/ && \
cp static/bg.png node_modules/ && \
cp static/styles.css node_modules/ && \
cp static/terminal.js node_modules/ && \
cp static/favicon.png node_modules/
# ============================================================================
# Stage 3: Runtime Image
# ============================================================================
FROM debian:trixie-slim FROM debian:trixie-slim
# Avoid prompts from apt # Avoid prompts from apt
ENV DEBIAN_FRONTEND=noninteractive ENV DEBIAN_FRONTEND=noninteractive
# Set environment variables
ENV RUST_VERSION=stable
ENV CARGO_HOME=/usr/local/cargo
ENV RUSTUP_HOME=/usr/local/rustup
ENV PATH=/usr/local/cargo/bin:$PATH
ENV TERM=xterm-256color ENV TERM=xterm-256color
# Install only runtime dependencies # Install system dependencies and security updates
RUN apt-get update && \ RUN apt-get update && \
apt-get upgrade -y && \ apt-get upgrade -y && \
apt-get install -y --no-install-recommends \ apt-get install -y \
# Runtime libraries # Build dependencies
libssl3 \ build-essential \
pkg-config \
libssl-dev \
# Rust/Cargo (needed to build webterm)
curl \
ca-certificates \ ca-certificates \
# For socktop packages # Node.js and npm (for xterm.js)
curl \ nodejs \
npm \
# Alacritty dependencies
cmake \
fontconfig \
libfontconfig1-dev \
libfreetype6-dev \
libxcb-xfixes0-dev \
libxkbcommon-dev \
python3 \
# Runtime dependencies
fonts-liberation \
gnupg2 \ gnupg2 \
# Shell and utilities wget \
bash \ unzip \
procps \ git \
# Health check # Process management
curl \ supervisor \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
# Add socktop APT repository and install packages # Install Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain ${RUST_VERSION} --profile minimal && \
chmod -R a+w ${RUSTUP_HOME} ${CARGO_HOME}
# Install Alacritty
RUN cargo install alacritty && \
rm -rf ${CARGO_HOME}/registry ${CARGO_HOME}/git
# Download and install FiraCode Nerd Font
RUN mkdir -p /usr/share/fonts/truetype/firacode-nerd && \
cd /tmp && \
wget -q https://github.com/ryanoasis/nerd-fonts/releases/download/v3.1.1/FiraCode.zip && \
unzip -q FiraCode.zip -d /usr/share/fonts/truetype/firacode-nerd/ && \
rm FiraCode.zip && \
fc-cache -fv && \
rm -rf /var/lib/apt/lists/*
# Add socktop APT repository with GPG key
RUN curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | \ RUN curl -fsSL https://jasonwitty.github.io/socktop/KEY.gpg | \
gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg && \ gpg --dearmor -o /usr/share/keyrings/socktop-archive-keyring.gpg && \
echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" > /etc/apt/sources.list.d/socktop.list && \ echo "deb [signed-by=/usr/share/keyrings/socktop-archive-keyring.gpg] https://jasonwitty.github.io/socktop stable main" > /etc/apt/sources.list.d/socktop.list && \
apt-get update && \ apt-get update && \
apt-get install -y --no-install-recommends socktop socktop-agent && \ apt-get install -y socktop socktop-agent && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
# Create application user (if not already exists from socktop packages) # Create application user (if not already exists from package)
RUN id -u socktop &>/dev/null || useradd -m -s /bin/bash socktop && \ RUN id -u socktop &>/dev/null || useradd -m -s /bin/bash socktop && \
mkdir -p /home/socktop/.config/alacritty && \
mkdir -p /home/socktop/.config/socktop && \ mkdir -p /home/socktop/.config/socktop && \
chown -R socktop:socktop /home/socktop chown -R socktop:socktop /home/socktop
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app
# Copy built binary from rust-builder # Copy application files
COPY --from=rust-builder /build/target/release/webterm-server /usr/local/bin/webterm-server COPY --chown=socktop:socktop Cargo.toml Cargo.lock ./
COPY --chown=socktop:socktop src ./src
COPY --chown=socktop:socktop templates ./templates
COPY --chown=socktop:socktop static ./static
COPY --chown=socktop:socktop package.json package-lock.json ./
# Copy templates and static files # Build the Rust application
COPY --from=rust-builder /build/templates ./templates RUN cargo build --release && \
COPY --from=rust-builder /build/static ./static rm -rf target/release/build target/release/deps target/release/incremental && \
strip target/release/webterm-server
# Copy node_modules from node-builder # Install npm dependencies and copy static files
COPY --from=node-builder /build/node_modules ./node_modules RUN npm ci --only=production && \
cp static/terminado-addon.js node_modules/ && \
cp static/bg.png node_modules/ && \
cp static/styles.css node_modules/ && \
cp static/terminal.js node_modules/ && \
cp static/favicon.png node_modules/
# Copy runtime scripts # Copy configuration files from /files directory (will be mounted as volume)
# This will be done at runtime via entrypoint script
# Copy supervisor configuration
COPY docker/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Copy entrypoint and restricted shell scripts
COPY docker/entrypoint.sh /entrypoint.sh COPY docker/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh COPY docker/restricted-shell.sh /usr/local/bin/restricted-shell
RUN chmod +x /entrypoint.sh && chmod +x /usr/local/bin/restricted-shell
# Expose ports # Expose ports
# 8082 - webterm HTTP server # 8082 - webterm HTTP server
# 3001 - socktop agent (if used) # 3001 - socktop agent
EXPOSE 8082 3001 EXPOSE 8082 3001
# Health check # Health check
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
# Run as socktop user # Set entrypoint (runs as root, then switches to socktop user)
USER socktop
# Set entrypoint
ENTRYPOINT ["/entrypoint.sh"] ENTRYPOINT ["/entrypoint.sh"]
# Default command - run webterm server # Default command (can be overridden)
CMD ["webterm-server", "--host", "0.0.0.0", "--port", "8082"] CMD ["supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

View File

@ -137,74 +137,6 @@ This project includes a complete CI/CD pipeline using Gitea Actions:
- **Containerization**: Docker, Kubernetes/k3s - **Containerization**: Docker, Kubernetes/k3s
- **CI/CD**: Gitea Actions - **CI/CD**: Gitea Actions
## Security and Limitations
### Security Model
This application is designed to provide **safe, public terminal access** for demonstration purposes. The security model consists of multiple layers:
#### 1. Restricted Shell
When deployed in production (e.g., https://www.socktop.io), the application uses a restricted shell (`docker/restricted-shell.sh`) that:
- **Allows only 2 commands**: `socktop` and `help`
- **Blocks all other commands**: Rejects any attempt to run `ls`, `cat`, `bash`, etc.
- **Validates arguments**: All arguments passed to `socktop` are sanitized to prevent command injection
- **Prevents shell escapes**: Blocks metacharacters like `;`, `&&`, `|`, `$()`, backticks, etc.
- **Blocks path traversal**: Prevents attempts like `../../../etc/passwd`
**Example validation:**
```bash
# Allowed
socktop -P local
socktop -P rpi-master
socktop ws://192.168.1.100:3000
# Blocked with error message
socktop $(whoami) # Command substitution blocked
socktop ; /bin/bash # Shell metacharacter blocked
socktop -P ../etc/passwd # Path traversal blocked
```
#### 2. Argument Sanitization
The restricted shell validates all input using regex patterns:
- Profile names: Only `[a-zA-Z0-9_-]+` characters allowed
- WebSocket URLs: Only `ws://` or `wss://` with safe characters
- No environment variable expansion
- No special characters or shell operators
**Security testing**: Run `./scripts/test-shell-security.sh` to verify all 35 security checks pass.
#### 3. Container Isolation
- Runs inside Docker container (cannot access host system)
- Non-root user (`socktop` user)
- Limited resources (CPU/memory limits configurable)
- No privileged operations
- Read-only configuration mounts
#### 4. WebSocket Security
The WebSocket endpoint (`/websocket`) is secure by design:
- Command spawned is **hardcoded at server startup** (via `--command` flag)
- No way to change which shell is spawned via WebSocket connection
- No HTTP headers or request parameters influence the spawned command
- Direct WebSocket connections get the same restricted shell as browser connections
**The Terminado protocol only supports:**
- `stdin` - Send input to terminal (goes through restricted shell)
- `stdout` - Receive output from terminal
- `set_size` - Resize terminal (validated to u16 row/col numbers only)
There is no protocol message type that can bypass the shell or execute arbitrary commands.
### Reporting Security Issues
If you discover a security vulnerability that bypasses the restricted shell or container isolation, please report it via:
- GetTea Issues (for non-critical issues)
- Direct contact to maintainer (for critical vulnerabilities)
## Credits ## Credits
This project was originally forked from [webterm](https://github.com/fubarnetes/webterm) by Fabian Freyer. I chose this as a base because other projects were way too complex. His project was simple and easy to understand, but it was not well maintained. I have updated it to modern packages and heavily customized it to display a working demo for socktop. This project was originally forked from [webterm](https://github.com/fubarnetes/webterm) by Fabian Freyer. I chose this as a base because other projects were way too complex. His project was simple and easy to understand, but it was not well maintained. I have updated it to modern packages and heavily customized it to display a working demo for socktop.

View File

@ -13,16 +13,8 @@ services:
network_mode: "host" network_mode: "host"
volumes: volumes:
# Mount configuration files directly to proper locations # Mount configuration files from host (read-write so root can access them)
- ./files/alacritty.toml:/home/socktop/.config/alacritty/alacritty.toml:ro - ./files:/files
- ./files/catppuccin-frappe.toml:/home/socktop/.config/alacritty/catppuccin-frappe.toml:ro
- ./files/profiles.json:/home/socktop/.config/socktop/profiles.json:ro
# Mount SSH certificates (optional - comment out if not using)
- ./files/rpi-master.pem:/home/socktop/.config/socktop/certs/rpi-master.pem:ro
- ./files/rpi-worker-1.pem:/home/socktop/.config/socktop/certs/rpi-worker-1.pem:ro
- ./files/rpi-worker-2.pem:/home/socktop/.config/socktop/certs/rpi-worker-2.pem:ro
- ./files/rpi-worker-3.pem:/home/socktop/.config/socktop/certs/rpi-worker-3.pem:ro
# Optional: persist socktop data # Optional: persist socktop data
- socktop-data:/home/socktop/.local/share/socktop - socktop-data:/home/socktop/.local/share/socktop

View File

@ -132,35 +132,13 @@ main() {
case "$cmd" in case "$cmd" in
socktop) socktop)
# Allow socktop with validated arguments only # Allow socktop with any arguments
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 -P local
else else
# Validate and sanitize arguments to prevent command injection # Pass arguments to socktop
# Only allow: -P <profile_name> or ws://<url> /usr/bin/socktop $args
# Check for profile argument (-P followed by safe profile name)
if [[ "$args" =~ ^-P[[:space:]]+[a-zA-Z0-9_-]+$ ]]; then
# Extract profile name and validate it
profile=$(echo "$args" | sed 's/-P[[:space:]]\+//')
/usr/bin/socktop -P "$profile"
# Check for websocket URL (ws:// or wss://)
elif [[ "$args" =~ ^wss?://[a-zA-Z0-9\.\:/_-]+$ ]]; then
# Validate websocket URL format
/usr/bin/socktop "$args"
else
# Reject anything else as potentially dangerous
echo -e "${RED}Error:${NC} Invalid arguments for socktop"
echo -e "${YELLOW}Allowed usage:${NC}"
echo " socktop - Use default local profile"
echo " socktop -P <profile> - Use named profile (alphanumeric, dash, underscore only)"
echo " socktop <ws_url> - Connect to websocket URL (ws:// or wss://)"
echo ""
echo -e "${YELLOW}Examples:${NC}"
echo " socktop -P rpi-master"
echo " socktop ws://192.168.1.100:3000"
fi
fi fi
;; ;;
help|--help|-h) help|--help|-h)

View File

@ -2,23 +2,6 @@
This directory contains configuration files that will be mounted into the Docker container at runtime. This directory contains configuration files that will be mounted into the Docker container at runtime.
## Docker Usage
When using Docker (via `docker-compose.yml` or `scripts/docker-quickstart.sh`):
1. **Files are mounted directly** to the proper locations in the container:
- `alacritty.toml``/home/socktop/.config/alacritty/alacritty.toml`
- `catppuccin-frappe.toml``/home/socktop/.config/alacritty/catppuccin-frappe.toml`
- `profiles.json``/home/socktop/.config/socktop/profiles.json`
- `*.pem` certificates → `/home/socktop/.config/socktop/certs/`
2. **Files are read-only** in the container (mounted with `:ro` flag)
3. **To update configuration**:
- Edit files in this directory on your host
- Changes are immediately visible in the container (no restart needed for most configs)
- For some changes, restart may be needed: `docker-compose restart` or `scripts/docker-quickstart.sh restart`
## Required Files ## Required Files
Place your actual configuration files in this directory before building/running the container: Place your actual configuration files in this directory before building/running the container:
@ -42,27 +25,11 @@ Place your actual configuration files in this directory before building/running
- Copy from: `profiles.json.example` - Copy from: `profiles.json.example`
- Update with your actual host IPs and connection details - Update with your actual host IPs and connection details
### 3. SSH Certificates (Optional) ### 3. SSH Keys
**`rpi-master.pem`** **`rpi-master.pem`**
- SSH private key for master node - SSH private key for master node
- **Permissions**: Must be `600` (will be auto-fixed by entrypoint) - **IMPORTANT**: Set permissions to 600
- Only needed if connecting to remote systems
**`rpi-worker-1.pem`, `rpi-worker-2.pem`, `rpi-worker-3.pem`**
- SSH private keys for worker nodes
- **Permissions**: Must be `600`
- Optional - add as needed for your systems
**Note**: If no certificates are provided, the container will still work for local monitoring.
### 4. Docker-Specific Notes
- Files are mounted directly from this directory to their final locations in the container
- Files are mounted read-only (`:ro`) for security
- Certificate permissions should be `600` on the host before mounting
- For local testing, you can comment out the certificate mounts in docker-compose.yml
- Without certificates, the container will still work for local monitoring
**`rpi-worker-1.pem`** **`rpi-worker-1.pem`**
- SSH private key for worker node 1 - SSH private key for worker node 1

View File

@ -1,152 +0,0 @@
#!/bin/bash
# Security test script for restricted shell
# Tests various injection and escape attempts
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo -e "${BLUE}╔════════════════════════════════════════════════════════╗${NC}"
echo -e "${BLUE}║ Restricted Shell Security Test ║${NC}"
echo -e "${BLUE}╚════════════════════════════════════════════════════════╝${NC}"
echo ""
PASSED=0
FAILED=0
TOTAL=0
# Function to test a command
test_command() {
local test_name="$1"
local test_input="$2"
local should_block="$3" # "block" or "allow"
TOTAL=$((TOTAL + 1))
echo -ne "${YELLOW}Testing:${NC} $test_name ... "
# Note: This is a template. In practice, you'd need to:
# 1. Send input to the restricted shell
# 2. Check if it was blocked or executed
# 3. Verify no unauthorized commands ran
# For now, we'll test the regex patterns
if [[ "$should_block" == "block" ]]; then
# These should be blocked
if [[ "$test_input" =~ ^-P[[:space:]]+[a-zA-Z0-9_-]+$ ]] || \
[[ "$test_input" =~ ^wss?://[a-zA-Z0-9\.\:/_-]+$ ]]; then
echo -e "${RED}FAIL${NC} - Should have blocked but pattern matched"
FAILED=$((FAILED + 1))
else
echo -e "${GREEN}PASS${NC} - Correctly blocked"
PASSED=$((PASSED + 1))
fi
else
# These should be allowed
if [[ "$test_input" =~ ^-P[[:space:]]+[a-zA-Z0-9_-]+$ ]] || \
[[ "$test_input" =~ ^wss?://[a-zA-Z0-9\.\:/_-]+$ ]]; then
echo -e "${GREEN}PASS${NC} - Correctly allowed"
PASSED=$((PASSED + 1))
else
echo -e "${RED}FAIL${NC} - Should have allowed but pattern didn't match"
FAILED=$((FAILED + 1))
fi
fi
}
echo -e "${BLUE}═══ Testing Valid Commands (Should Allow) ═══${NC}"
echo ""
test_command "Local profile" "-P local" "allow"
test_command "Remote profile" "-P rpi-master" "allow"
test_command "Profile with dash" "-P rpi-worker-1" "allow"
test_command "Profile with underscore" "-P my_profile" "allow"
test_command "Websocket URL" "ws://192.168.1.100:3000" "allow"
test_command "Secure websocket" "wss://example.com:3000" "allow"
test_command "Websocket with path" "ws://192.168.1.100:3000/ws" "allow"
echo ""
echo -e "${BLUE}═══ Testing Command Injection (Should Block) ═══${NC}"
echo ""
test_command "Command substitution \$()" "-P \$(whoami)" "block"
test_command "Command substitution backticks" "-P \`id\`" "block"
test_command "Shell semicolon" "-P local; ls -la" "block"
test_command "Shell AND operator" "-P local && cat /etc/passwd" "block"
test_command "Shell OR operator" "-P local || /bin/sh" "block"
test_command "Shell pipe" "-P local | grep root" "block"
test_command "Shell redirect" "-P local > /tmp/output" "block"
test_command "Shell background" "-P local &" "block"
echo ""
echo -e "${BLUE}═══ Testing Path Traversal (Should Block) ═══${NC}"
echo ""
test_command "Parent directory" "-P ../etc/passwd" "block"
test_command "Absolute path" "-P /etc/passwd" "block"
test_command "Multiple parent dirs" "-P ../../bin/bash" "block"
test_command "Encoded path" "-P %2e%2e%2f" "block"
echo ""
echo -e "${BLUE}═══ Testing Special Characters (Should Block) ═══${NC}"
echo ""
test_command "Newline injection" "-P local\nls" "block"
test_command "Carriage return" "-P local\rls" "block"
test_command "Null byte" "-P local\x00ls" "block"
test_command "Single quote" "-P local' ls" "block"
test_command "Double quote" "-P local\" ls" "block"
test_command "Dollar sign" "-P \$HOME" "block"
test_command "Asterisk wildcard" "-P local*" "block"
test_command "Question wildcard" "-P local?" "block"
echo ""
echo -e "${BLUE}═══ Testing Environment Variables (Should Block) ═══${NC}"
echo ""
test_command "HOME variable" "-P \$HOME" "block"
test_command "PATH variable" "-P \$PATH" "block"
test_command "SHELL variable" "-P \$SHELL" "block"
test_command "Braced variable" "-P \${HOME}" "block"
echo ""
echo -e "${BLUE}═══ Testing WebSocket URL Exploits (Should Block) ═══${NC}"
echo ""
test_command "WS with command injection" "ws://evil.com/\$(id)" "block"
test_command "WS with backticks" "ws://evil.com/\`whoami\`" "block"
test_command "WS with semicolon" "ws://evil.com/; ls" "block"
test_command "WS with spaces" "ws://evil.com/ /bin/sh" "block"
echo ""
echo -e "${BLUE}════════════════════════════════════════════════════${NC}"
echo -e "${BLUE} TEST SUMMARY ${NC}"
echo -e "${BLUE}════════════════════════════════════════════════════${NC}"
echo ""
echo -e "Total Tests: ${BLUE}$TOTAL${NC}"
echo -e "Passed: ${GREEN}$PASSED${NC}"
echo -e "Failed: ${RED}$FAILED${NC}"
echo ""
if [ $FAILED -eq 0 ]; then
echo -e "${GREEN}✓ All security tests passed!${NC}"
echo ""
echo -e "${YELLOW}Note:${NC} These are pattern validation tests only."
echo "For complete security verification, you should:"
echo " 1. Test in actual container environment"
echo " 2. Verify socktop binary doesn't process malicious args"
echo " 3. Monitor for unexpected process execution"
echo " 4. Check logs for injection attempts"
echo ""
exit 0
else
echo -e "${RED}✗ Some security tests failed!${NC}"
echo ""
echo "Review the failed tests and update regex patterns in restricted-shell.sh"
echo ""
exit 1
fi