mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
fix(security): remove CSP upgrade-insecure-requests and HSTS over HTTP
Helmet's defaults are designed for HTTPS-only deployments. Two directives were actively breaking plain-HTTP self-hosted instances: - upgrade-insecure-requests: causes browsers to upgrade all JS/CSS/asset fetches to HTTPS → ERR_SSL_PROTOCOL_ERROR → completely blank page - Strict-Transport-Security: permanently instructs browsers to refuse HTTP for 1 year, even after the header is removed from the server Also handle docker socket GID=0 (root:root) edge case in entrypoint and add [entrypoint] diagnostic log lines for easier debugging.
This commit is contained in:
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- **Blank page on HTTP deployments (CSP `upgrade-insecure-requests`):** Helmet's default CSP included `upgrade-insecure-requests`, which causes browsers to silently upgrade all HTTP sub-resource fetches (JS, CSS, images) to HTTPS. On a plain-HTTP self-hosted deployment this produces a completely blank page with `ERR_SSL_PROTOCOL_ERROR`. The directive is now explicitly omitted from the CSP.
|
||||
- **HSTS permanently breaking HTTP access:** Helmet's default `Strict-Transport-Security` header was being sent over HTTP, instructing browsers to refuse non-HTTPS connections for 1 year. HSTS is now disabled and must only be re-enabled by users who terminate HTTPS at a reverse proxy in front of Sencho.
|
||||
- **Docker socket EACCES root:root edge case:** Entrypoint now handles the case where the Docker socket is owned by root:root (GID 0) in addition to the standard root:docker case. Diagnostic `[entrypoint]` log lines are emitted at startup to make group detection visible in `docker logs`.
|
||||
|
||||
### Added
|
||||
- **Resources Hub — Managed/Unmanaged Separation:** All Docker resources (images, volumes, networks) are now classified as `managed` (belonging to a Sencho stack), `external` (belonging to another Compose project), or `unused`/`system`. Classification is exposed via a new `GET /api/system/resources` endpoint that makes 4 parallel Docker API calls once and returns all three resource types in a single round trip.
|
||||
- **Docker Disk Footprint widget:** Replaces the Reclaimable Space donut chart with an interactive horizontal stacked bar showing Sencho Managed vs External Projects vs Reclaimable bytes. Clicking a segment filters the Images and Volumes tabs simultaneously.
|
||||
|
||||
+26
-3
@@ -65,9 +65,32 @@ const getCookieOptions = (req: Request) => ({
|
||||
// Middleware
|
||||
|
||||
// Security headers (X-Frame-Options, X-Content-Type-Options, etc.)
|
||||
// crossOriginEmbedderPolicy is disabled because the Monaco editor uses workers
|
||||
// that don't set the required COEP headers.
|
||||
app.use(helmet({ crossOriginEmbedderPolicy: false }));
|
||||
// crossOriginEmbedderPolicy: disabled — Monaco editor workers lack COEP headers.
|
||||
// hsts: disabled — HSTS must only be set when the app is served over HTTPS.
|
||||
// Enabling it over HTTP permanently breaks browser access for 1 year.
|
||||
// contentSecurityPolicy.upgrade-insecure-requests: removed — this directive
|
||||
// tells browsers to silently upgrade all HTTP sub-resource fetches to HTTPS.
|
||||
// On a plain-HTTP self-hosted deployment (the common case) this causes every
|
||||
// JS/CSS asset to fail with ERR_SSL_PROTOCOL_ERROR, producing a blank page.
|
||||
app.use(helmet({
|
||||
crossOriginEmbedderPolicy: false,
|
||||
hsts: false,
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
baseUri: ["'self'"],
|
||||
fontSrc: ["'self'", 'https:', 'data:'],
|
||||
formAction: ["'self'"],
|
||||
frameAncestors: ["'self'"],
|
||||
imgSrc: ["'self'", 'data:'],
|
||||
objectSrc: ["'none'"],
|
||||
scriptSrc: ["'self'"],
|
||||
scriptSrcAttr: ["'none'"],
|
||||
styleSrc: ["'self'", 'https:', "'unsafe-inline'"],
|
||||
// 'upgrade-insecure-requests' is intentionally absent — see comment above.
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
// CORS — in production restrict to the configured frontend origin.
|
||||
// In development, mirror the request origin so Vite's dev server works.
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Resolve the data directory, mirroring DatabaseService.ts logic.
|
||||
DATA_DIR="${DATA_DIR:-/app/data}"
|
||||
|
||||
# If running as root (the default Docker container start), fix volume ownership,
|
||||
# fix Docker socket group access, then drop privileges before executing the app.
|
||||
#
|
||||
# This is the industry-standard pattern used by the official PostgreSQL, Redis,
|
||||
# and MariaDB Docker images, and by Docker management tools like Portainer and
|
||||
# Dockge that also require access to /var/run/docker.sock as a non-root user.
|
||||
#
|
||||
# The UID guard also ensures compatibility with strict environments like
|
||||
# Kubernetes (runAsNonRoot: true) or OpenShift, where the container is forced to
|
||||
# run as a random high UID. In that case both blocks are skipped and the app
|
||||
# exec's directly without crashing.
|
||||
if [ "$(id -u)" = '0' ]; then
|
||||
|
||||
# ── 1. Fix data volume ownership ────────────────────────────────────────
|
||||
# Handles host volumes previously created by root or a different UID, which
|
||||
# would cause SQLITE_READONLY errors when the non-root sencho user starts.
|
||||
# Only touches files with wrong user OR group (efficient on large dirs).
|
||||
mkdir -p "$DATA_DIR"
|
||||
find "$DATA_DIR" \( \! -user sencho -o \! -group sencho \) \
|
||||
-exec chown sencho:sencho '{}' +
|
||||
echo "[entrypoint] Data directory ownership ensured: $DATA_DIR"
|
||||
|
||||
# ── 2. Fix Docker socket group access ───────────────────────────────────
|
||||
# The Docker socket on the host is owned by the host's docker group, whose
|
||||
# GID varies by Linux distribution and does not match any group inside the
|
||||
# container by default.
|
||||
#
|
||||
# Solution: detect the socket's GID at runtime, create a matching group
|
||||
# inside the container if one doesn't already exist, then add sencho to it.
|
||||
# su-exec calls getgrouplist() which reads /etc/group, so supplementary
|
||||
# groups added here ARE inherited by the Node process.
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
DOCKER_SOCK_GID=$(stat -c '%g' /var/run/docker.sock)
|
||||
DOCKER_SOCK_MODE=$(stat -c '%a' /var/run/docker.sock)
|
||||
echo "[entrypoint] Docker socket found: GID=$DOCKER_SOCK_GID mode=$DOCKER_SOCK_MODE"
|
||||
|
||||
if [ "$DOCKER_SOCK_GID" = "0" ]; then
|
||||
# Socket is owned by root:root. The only way to access it without
|
||||
# running as root is to make it group-readable and assign a shared
|
||||
# group. We create a 'docker-sock' group with GID 0 for clarity.
|
||||
echo "[entrypoint] WARNING: Docker socket is root:root — adding sencho to root group"
|
||||
addgroup sencho root 2>/dev/null || true
|
||||
else
|
||||
if ! getent group "$DOCKER_SOCK_GID" > /dev/null 2>&1; then
|
||||
addgroup -S -g "$DOCKER_SOCK_GID" docker-host
|
||||
echo "[entrypoint] Created group docker-host with GID $DOCKER_SOCK_GID"
|
||||
fi
|
||||
DOCKER_GROUP=$(getent group "$DOCKER_SOCK_GID" | cut -d: -f1)
|
||||
addgroup sencho "$DOCKER_GROUP" 2>/dev/null || true
|
||||
echo "[entrypoint] Added sencho to group '$DOCKER_GROUP' (GID $DOCKER_SOCK_GID)"
|
||||
fi
|
||||
else
|
||||
echo "[entrypoint] WARNING: /var/run/docker.sock not found — Docker features will be unavailable"
|
||||
fi
|
||||
|
||||
echo "[entrypoint] Dropping privileges to sencho (uid=$(id -u sencho))"
|
||||
|
||||
# ── 3. Drop privileges ──────────────────────────────────────────────────
|
||||
# Replace this shell process with su-exec so that Node becomes PID 1 and
|
||||
# receives SIGTERM/SIGINT directly from Docker. su-exec calls getgrouplist()
|
||||
# for named users, so all supplementary groups set above are inherited.
|
||||
exec su-exec sencho "$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
Reference in New Issue
Block a user