-
-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathdocker-entrypoint.sh
More file actions
296 lines (288 loc) · 12.8 KB
/
Copy pathdocker-entrypoint.sh
File metadata and controls
296 lines (288 loc) · 12.8 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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
#!/bin/sh
# Entrypoint: make the persistent data volume writable, then drop privileges.
#
# Railway (and most managed hosts) mount a persistent volume owned by root. Engraphis
# runs as the non-root `engraphis` user (see Dockerfile), so without this the app cannot
# create /data/engraphis.db or customer state under /data/.engraphis and crashes at
# startup with `sqlite3.OperationalError: unable to open database file`.
#
# We therefore start the container as root, repair ownership once, and exec the real command
# as `engraphis` via gosu — keeping the deliberate non-root runtime while making the volume
# writable. A marker avoids repeating ownership writes on a correctly owned volume.
# A non-root launch initializes private settings in its already-writable state volume.
set -e
umask 077
# Default bind host, decided at runtime (not baked into the image). Uvicorn's `::`
# listener is IPv6-only on some container kernels, so plain Docker port forwarding cannot
# reach it over IPv4. Railway injects RAILWAY_SERVICE_NAME into every deployment and needs
# IPv6 for its private-network healthchecks; ordinary Docker runs bind 0.0.0.0 instead.
# An operator-provided ENGRAPHIS_HOST always wins.
if [ -z "${ENGRAPHIS_HOST:-}" ]; then
if [ -n "${RAILWAY_SERVICE_NAME:-}" ] && [ -f /proc/net/if_inet6 ]; then
ENGRAPHIS_HOST="::"
else
ENGRAPHIS_HOST="0.0.0.0"
fi
export ENGRAPHIS_HOST
fi
# Validate every existing component without resolving through a symlink. The trusted
# config path is operator-configured and may be outside /data, so checking only its
# leaf or final parent would let an app-writable intermediate directory redirect root's
# chmod/chown into the image. Reject dot-dot paths rather than guessing their target.
reject_linked_path() {
path=$1
case "$path" in
/*) ;;
*) return 1 ;;
esac
remainder=${path#/}
current=
while [ -n "$remainder" ]; do
case "$remainder" in
*/*)
component=${remainder%%/*}
remainder=${remainder#*/}
;;
*)
component=$remainder
remainder=
;;
esac
case "$component" in
""|.) continue ;;
..) return 1 ;;
esac
if [ -n "$current" ]; then
current="$current/$component"
else
current="/$component"
fi
if [ -L "$current" ]; then
return 1
fi
done
return 0
}
state_directory_is_owned() {
# Only /data is an ownership-repair target. External state paths must be
# provisioned for the app beforehand, including on the first boot.
case "$1" in
/data|/data/*) return 0 ;;
esac
[ -d "$1" ] && [ "$(stat -c '%u' "$1" 2>/dev/null)" = "$2" ]
}
create_private_runtime_directory() {
if ! reject_linked_path "$1"; then
return 1
fi
create_remaining=${1#/}
create_current=
while [ -n "$create_remaining" ]; do
case "$create_remaining" in
*/*)
create_component=${create_remaining%%/*}
create_remaining=${create_remaining#*/}
;;
*)
create_component=$create_remaining
create_remaining=
;;
esac
case "$create_component" in ""|.) continue ;; esac
create_current="$create_current/$create_component"
if [ ! -e "$create_current" ]; then
# Never chown an existing ancestor. Each new directory must become
# traversable by the app before another private child is created.
if ! mkdir "$create_current" || ! reject_linked_path "$create_current" \
|| [ ! -d "$create_current" ] \
|| ! chown engraphis:engraphis "$create_current"; then
return 1
fi
elif [ ! -d "$create_current" ] || [ -L "$create_current" ]; then
return 1
fi
done
}
repair_volume_descendants() {
# Restores can preserve the marker while resetting file ownership. Scan for
# a mismatch before trusting it; do not follow links outside the volume.
unowned_entry=$(find "$1" ! -uid "$2" -print -quit) || return 1
if [ -n "$unowned_entry" ]; then
chown -R -h engraphis:engraphis "$1" || return 1
fi
}
if [ "$(id -u)" = "0" ]; then
# ENGRAPHIS_STATE_DIR defaults to /data/.engraphis. Repair the complete volume only on
# first boot or ownership drift; restarts scan without rewriting correct ownership.
state_dir="${ENGRAPHIS_STATE_DIR:-/data/.engraphis}"
# Keep the marker on the volume it describes; external state may outlive a
# replaced /data volume that still needs its first ownership repair.
ownership_marker="/data/.volume-ownership"
config_file="${ENGRAPHIS_ENV_FILE:-}"
app_owner=$(id -u engraphis)
if ! reject_linked_path "$state_dir"; then
printf '%s\n' "[engraphis] refusing linked or unnormalized state path: $state_dir" >&2
exit 1
fi
# The state directory is app-writable after first boot. Reject a planted link or
# non-directory before mkdir/chown can follow it into a root-owned image path.
if [ -L "$state_dir" ]; then
printf '%s\n' "[engraphis] refusing symlinked state directory: $state_dir" >&2
exit 1
elif [ -e "$state_dir" ] && [ ! -d "$state_dir" ]; then
printf '%s\n' "[engraphis] refusing non-directory state path: $state_dir" >&2
exit 1
fi
if ! state_directory_is_owned "$state_dir" "$app_owner"; then
printf '%s\n' "[engraphis] external state directory must already be owned by engraphis: $state_dir" >&2
exit 1
fi
if ! create_private_runtime_directory "$state_dir"; then
printf '%s\n' "[engraphis] unable to create state directory: $state_dir" >&2
exit 1
fi
if ! reject_linked_path "$state_dir" || [ ! -d "$state_dir" ] || ! chmod 700 "$state_dir"; then
printf '%s\n' "[engraphis] unable to restrict state directory: $state_dir" >&2
exit 1
fi
if [ -n "$config_file" ]; then
if ! reject_linked_path "$config_file"; then
printf '%s\n' "[engraphis] refusing linked or unnormalized trusted config path: $config_file" >&2
exit 1
fi
config_parent=$(dirname "$config_file")
if [ -L "$config_parent" ]; then
printf '%s\n' "[engraphis] refusing symlinked trusted config directory: $config_parent" >&2
exit 1
fi
if [ -e "$config_parent" ] && [ ! -d "$config_parent" ]; then
printf '%s\n' "[engraphis] refusing non-directory trusted config parent: $config_parent" >&2
exit 1
fi
if ! create_private_runtime_directory "$config_parent"; then
printf '%s\n' "[engraphis] unable to create config directory: $config_parent" >&2
exit 1
fi
# Check external existing parents before creating or changing any file.
# Parents under /data receive the volume's first-boot ownership repair.
case "$config_parent" in
/data|/data/*) ;;
*)
config_owner=$(stat -c '%u' "$config_parent" 2>/dev/null || true)
if [ "$config_owner" != "$app_owner" ]; then
printf '%s\n' "[engraphis] trusted config directory must be owned by engraphis: $config_parent" >&2
exit 1
fi
;;
esac
if ! reject_linked_path "$config_file"; then
printf '%s\n' "[engraphis] refusing symlinked trusted config file: $config_file" >&2
exit 1
fi
if [ ! -e "$config_file" ] && ! : > "$config_file"; then
printf '%s\n' "[engraphis] unable to create trusted config file: $config_file" >&2
exit 1
fi
if ! reject_linked_path "$config_file" || [ ! -f "$config_file" ] \
|| [ "$(stat -c '%h' "$config_file" 2>/dev/null)" != "1" ]; then
printf '%s\n' "[engraphis] refusing changed, hard-linked, or non-regular trusted config file: $config_file" >&2
exit 1
fi
if ! chmod 600 "$config_file"; then
printf '%s\n' "[engraphis] unable to restrict trusted config file: $config_file" >&2
exit 1
fi
fi
# The app user owns the persistent marker after first boot. Fail closed if it has
# replaced that trusted root-startup input with a symlink or a non-regular path:
# chown follows symlinks by default and would otherwise let the marker redirect
# root's ownership change to an arbitrary target on the mounted volume.
if [ -L "$ownership_marker" ] || ! reject_linked_path "$ownership_marker"; then
printf '%s\n' "[engraphis] refusing symlinked volume ownership marker: $ownership_marker" >&2
exit 1
fi
if [ ! -e "$ownership_marker" ]; then
if ! chown -R -h engraphis:engraphis /data; then
printf '%s\n' "[engraphis] unable to repair /data ownership" >&2
exit 1
fi
if ! : > "$ownership_marker"; then
printf '%s\n' "[engraphis] unable to create volume ownership marker" >&2
exit 1
fi
if ! chown engraphis:engraphis "$ownership_marker"; then
printf '%s\n' "[engraphis] unable to own volume ownership marker" >&2
exit 1
fi
elif [ ! -f "$ownership_marker" ]; then
printf '%s\n' "[engraphis] refusing non-regular volume ownership marker: $ownership_marker" >&2
exit 1
elif [ "$(stat -c '%h' "$ownership_marker" 2>/dev/null)" != "1" ]; then
printf '%s\n' "[engraphis] refusing hard-linked volume ownership marker: $ownership_marker" >&2
exit 1
elif ! repair_volume_descendants /data "$app_owner" \
|| ! chown engraphis:engraphis /data "$state_dir" "$ownership_marker"; then
printf '%s\n' "[engraphis] unable to verify /data ownership" >&2
exit 1
fi
if [ -n "$config_file" ]; then
# A pre-existing config directory may be a separate root-owned mount. Do not
# chown an arbitrary existing host path; fail closed if it is unusable instead
# of starting a dashboard whose settings silently cannot persist.
config_owner=$(stat -c '%u' "$config_parent" 2>/dev/null || true)
if [ -z "$config_owner" ] || [ "$config_owner" != "$app_owner" ]; then
printf '%s\n' "[engraphis] trusted config directory must be owned by engraphis: $config_parent" >&2
exit 1
fi
fi
if [ -n "$config_file" ]; then
if ! reject_linked_path "$config_file" || [ ! -f "$config_file" ] \
|| [ "$(stat -c '%h' "$config_file" 2>/dev/null)" != "1" ]; then
printf '%s\n' "[engraphis] refusing changed trusted config file: $config_file" >&2
exit 1
fi
if ! chown engraphis:engraphis "$config_file"; then
printf '%s\n' "[engraphis] unable to own trusted config file" >&2
exit 1
fi
fi
exec gosu engraphis "$@"
fi
# Explicit trusted settings are required by the configuration loader. A rootless
# container with a writable volume must provision them before the app imports it.
state_dir="${ENGRAPHIS_STATE_DIR:-/data/.engraphis}"
config_file="${ENGRAPHIS_ENV_FILE:-}"
if ! reject_linked_path "$state_dir"; then
printf '%s\n' "[engraphis] refusing linked or unnormalized state path: $state_dir" >&2
exit 1
fi
if ! mkdir -p "$state_dir" || ! reject_linked_path "$state_dir" || [ ! -d "$state_dir" ] || ! chmod 700 "$state_dir"; then
printf '%s\n' "[engraphis] unable to initialize private state directory: $state_dir" >&2
exit 1
fi
if [ -n "$config_file" ]; then
if ! reject_linked_path "$config_file"; then
printf '%s\n' "[engraphis] refusing linked or unnormalized trusted config path: $config_file" >&2
exit 1
fi
config_parent=$(dirname "$config_file")
if ! mkdir -p "$config_parent" || ! reject_linked_path "$config_parent" || [ ! -d "$config_parent" ]; then
printf '%s\n' "[engraphis] unable to initialize trusted config directory: $config_parent" >&2
exit 1
fi
config_owner=$(stat -c '%u' "$config_parent" 2>/dev/null || true)
if [ "$config_owner" != "$(id -u)" ]; then
printf '%s\n' "[engraphis] trusted config directory must belong to the runtime user: $config_parent" >&2
exit 1
fi
if [ ! -e "$config_file" ] && ! : > "$config_file"; then
printf '%s\n' "[engraphis] unable to create trusted config file: $config_file" >&2
exit 1
fi
if ! reject_linked_path "$config_file" || [ ! -f "$config_file" ] \
|| [ "$(stat -c '%h' "$config_file" 2>/dev/null)" != "1" ] || ! chmod 600 "$config_file"; then
printf '%s\n' "[engraphis] unable to restrict trusted config file: $config_file" >&2
exit 1
fi
fi
exec "$@"