Repository navigation
Expand file tree
/
Copy pathentrypoint.sh
More file actions
executable file
·165 lines (152 loc) · 7.81 KB
/
Copy pathentrypoint.sh
File metadata and controls
executable file
·165 lines (152 loc) · 7.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#!/usr/bin/env bash
# =============================================================================
# entrypoint.sh — Docker entrypoint for rmcp-template
#
# TEMPLATE: This script runs as root before dropping privileges to UID 1000.
# Copy it to the repo root and update the TEMPLATE sections below.
#
# Pattern §26: Every Docker image runs this entrypoint before the binary.
# 1. Creates the data directory if it doesn't exist
# 2. Fixes ownership so the service user can write files
# 3. Hardens file permissions on sensitive config
# 4. Validates required env vars (fast-fail with a clear error)
# 5. Drops to UID 1000:1000 and exec's the binary
#
# Dockerfile wires this in:
# COPY entrypoint.sh /entrypoint.sh
# RUN chmod +x /entrypoint.sh
# ENTRYPOINT ["/entrypoint.sh"]
# CMD ["example", "serve", "mcp"]
#
# The ENTRYPOINT + CMD split means:
# - `docker run image` → runs: /entrypoint.sh example serve mcp
# - `docker run image example --help` → runs: /entrypoint.sh example --help
# - `docker run image sh` → runs: /entrypoint.sh sh (useful for debugging)
#
# TEMPLATE: Update REQUIRED_VARS and binary name below.
# =============================================================================
set -euo pipefail
SERVICE_NAME="synapse"
BINARY="/usr/local/bin/${SERVICE_NAME}"
SYNAPSE_UID="${SYNAPSE_UID:-1000}"
SYNAPSE_GID="${SYNAPSE_GID:-1000}"
case "${SYNAPSE_UID}:${SYNAPSE_GID}" in
*[!0-9:]*|:*|*:) echo "ERROR: SYNAPSE_UID and SYNAPSE_GID must be numeric" >&2; exit 2 ;;
esac
# ── Data directory ─────────────────────────────────────────────────────────────
# TEMPLATE: DATA_DIR is the container's persistent storage path. It is always
# /data inside the container and bind-mounted from ~/.<service>/ on
# the host via docker-compose.yml.
# DO NOT change this to a non-/data path inside the container.
DATA_DIR="${DATA_DIR:-/data}"
# Create the data directory if it doesn't exist.
# This is idempotent — running twice is safe.
mkdir -p "${DATA_DIR}"
# Fix ownership so the service user (UID 1000) can write files when the mount
# permits it. Some host bind mounts reject recursive chown even when UID 1000
# already owns the directory, so keep startup tolerant and let the app's own
# write checks surface real permission problems.
if ! chown -R "${SYNAPSE_UID}:${SYNAPSE_GID}" "${DATA_DIR}" 2>/dev/null; then
echo "WARN: could not chown ${DATA_DIR} to ${SYNAPSE_UID}:${SYNAPSE_GID}; continuing" >&2
fi
# Restrict directory permissions when supported by the mount:
# 750 = owner rwx, group rx, others nothing
# This prevents other processes in the container from reading the data dir.
if ! chmod 750 "${DATA_DIR}" 2>/dev/null; then
echo "WARN: could not chmod ${DATA_DIR} to 750; continuing" >&2
fi
# ── Harden sensitive files ────────────────────────────────────────────────────
# If config.toml exists, make it group-readable but not world-readable.
# TEMPLATE: Add similar blocks for any other sensitive files your service writes.
if [ -f "${DATA_DIR}/config.toml" ]; then
chmod 640 "${DATA_DIR}/config.toml"
fi
# .env must not be world-readable — it contains API keys.
if [ -f "${DATA_DIR}/.env" ]; then
chmod 600 "${DATA_DIR}/.env"
fi
# JWT signing key — extremely sensitive; owner-only.
if [ -f "${DATA_DIR}/auth-jwt.pem" ]; then
chmod 600 "${DATA_DIR}/auth-jwt.pem"
fi
# Auth database — owner-only. lab-auth rejects any group/other bits
# (ensure_restrictive_permissions: mode & 0o077 must be 0), so 640 fails.
if [ -f "${DATA_DIR}/auth.db" ]; then
chmod 600 "${DATA_DIR}/auth.db"
fi
# ── Validate required environment variables ────────────────────────────────────
# TEMPLATE: Add your service's required env vars here.
# Comment out or remove lines for vars that have safe defaults.
# The goal: fail loudly here rather than silently misbehave later.
#
# Example (uncomment for a real service):
# if [ -z "${EXAMPLE_API_KEY:-}" ]; then
# echo "ERROR: EXAMPLE_API_KEY is not set." >&2
# echo " Set it in your .env file or Docker environment." >&2
# exit 1
# fi
#
# The template binary works without API credentials (stub mode), so no
# required vars are checked here. Uncomment the block above when you replace
# the stub with a real upstream service.
# ── Drop privileges and exec the service ──────────────────────────────────────
# `gosu` (Alpine) or `gosu` (Debian/Ubuntu) replaces the current process
# with the service binary running as UID 1000:1000.
#
# TEMPLATE: The Dockerfile installs gosu (Alpine) or gosu (Debian).
# The current Dockerfile uses Debian, so install gosu there:
# RUN apt-get install -y gosu
# and replace gosu below with gosu.
#
# `exec` is critical — it replaces the shell process so the binary receives
# OS signals (SIGTERM, SIGINT) directly. Without exec, the shell would buffer
# signals and Docker's stop timeout would kill the container ungracefully.
#
# TEMPLATE: Replace "gosu" with "gosu" if using a Debian-based image,
# or use "exec setpriv --reuid=1000 --regid=1000 --clear-groups" if
# neither gosu nor gosu is available.
# ── Docker socket group ───────────────────────────────────────────────────────
# When /var/run/docker.sock is bind-mounted (for the `flux` Docker tools), it is
# typically owned by root:docker on the host. The service runs as UID 1000, so it
# needs the socket's group as a supplementary group to reach the daemon. The GID
# varies per host. Production Compose starts as UID 1000 and supplies the group
# with `group_add`; direct `docker run` invocations that start as root can use
# this dynamic setpriv path. setpriv needs no writes to /etc, so it still works
# under a read-only root filesystem. When there is no socket (or setpriv is
# unavailable) we fall back to plain gosu 1000:1000.
DOCKER_SOCK="${DOCKER_SOCK:-/var/run/docker.sock}"
SOCK_GID=""
if [ -S "${DOCKER_SOCK}" ]; then
# Detect the GID that owns the docker socket so we can add it as a
# supplementary group when dropping privileges. If stat fails (e.g. the
# socket exists but is not readable before privilege drop), we fall back
# to gosu without the extra group — the service will still start but
# Docker operations requiring the socket will fail at runtime.
if _gid="$(stat -c '%g' "${DOCKER_SOCK}" 2>/dev/null)"; then
SOCK_GID="${_gid}"
else
echo "WARN: could not read GID of ${DOCKER_SOCK}; Docker socket access may fail" >&2
fi
fi
# drop_exec: re-exec "$@" as UID/GID 1000, preserving the docker socket group.
# `exec` is critical — it replaces the shell so the binary receives OS signals
# (SIGTERM/SIGINT) directly and Docker can stop the container gracefully.
drop_exec() {
if [ "$(id -u)" != "0" ]; then
exec "$@"
fi
if [ -n "${SOCK_GID}" ] && [ "${SOCK_GID}" != "0" ] && command -v setpriv >/dev/null 2>&1; then
exec setpriv --reuid "${SYNAPSE_UID}" --regid "${SYNAPSE_GID}" --groups "${SOCK_GID}" "$@"
fi
exec gosu "${SYNAPSE_UID}:${SYNAPSE_GID}" "$@"
}
# Passthrough: if the first argument is not a known subcommand (e.g. docker run ... bash),
# exec it directly without prepending the binary.
case "${1:-}" in
serve|mcp|flux|scout|greet|echo|status|watch|doctor|setup|help|--help|-h|--version|"")
drop_exec "${BINARY}" "$@"
;;
*)
drop_exec "$@"
;;
esac