mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
security(docker): non-root soundtouch-service prep, dormant behind a toggle (refs #451)
Lands the groundwork to run the service container as non-root, but keeps it
running as root by default so this is NOT a breaking change yet. Enabling it
(BREAKING) is planned for v1.0.0 and reduced to a one-line flip.
Image prep (all harmless while running as root):
- A fixed non-root user, uid/gid 65532 (aftertouch), with /app chowned to it.
- A cap_net_bind_service file capability on the binary so the optional DNS
server can still bind :53 as non-root (NET_BIND_SERVICE is in Docker's
default cap set; no --cap-add needed). Applied after chown so it survives.
- USER ${APP_USER} with ARG APP_USER=root: still root by default. To enable
non-root, flip the default to "aftertouch" (one line) or build with
--build-arg APP_USER=aftertouch.
Startup safety net (active now, no-op while writable):
- warnIfDataDirNotWritable probes DATA_DIR and, if it can't write, logs the
exact `chown -R 65532:65532 <dir>` fix (with the process uid) instead of
failing later with a cryptic permission error. This is the common snag when
a non-root container meets a bind-mounted host dir owned by someone else.
Verified: default build runs as root; --build-arg APP_USER=aftertouch runs as
65532, serves /health, writes the data dir; a read-only data dir triggers the
warning + chown hint.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f8f783428a
commit
0fd9ad7dad
+27
-1
@@ -50,6 +50,13 @@ FROM alpine:3.23 AS soundtouch-service
|
||||
|
||||
RUN apk add --no-cache ca-certificates tzdata
|
||||
|
||||
# Non-root prep (dormant). Everything below is set up so the service CAN run
|
||||
# as a fixed non-root user, but the image still runs as root by default
|
||||
# (APP_USER below) so this is not a breaking change yet. The UID/GID is pinned
|
||||
# (65532) so a mounted data volume's ownership stays predictable.
|
||||
RUN addgroup -g 65532 -S aftertouch \
|
||||
&& adduser -u 65532 -S -G aftertouch -H -h /app aftertouch
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY --from=builder /soundtouch-service /app/soundtouch-service
|
||||
@@ -57,13 +64,32 @@ COPY --from=builder /soundtouch-service /app/soundtouch-service
|
||||
# Verify the binary works on the target platform
|
||||
RUN /app/soundtouch-service version || echo "Binary verification complete"
|
||||
|
||||
RUN mkdir -p /app/data
|
||||
# Create the data dir and hand /app to the non-root user.
|
||||
RUN mkdir -p /app/data && chown -R aftertouch:aftertouch /app
|
||||
|
||||
# Allow the non-root process to bind the privileged DNS port (:53) when DNS
|
||||
# Discovery is enabled, without granting the whole container extra privileges
|
||||
# at runtime. NET_BIND_SERVICE is in Docker's default capability set, so this
|
||||
# file capability is effective out of the box (no --cap-add needed). Done
|
||||
# after chown, which would otherwise clear it; the setcap tool is removed after.
|
||||
RUN apk add --no-cache --virtual .setcap libcap \
|
||||
&& setcap 'cap_net_bind_service=+ep' /app/soundtouch-service \
|
||||
&& apk del .setcap
|
||||
|
||||
ENV PORT=8000
|
||||
ENV DATA_DIR=/app/data
|
||||
ENV LOG_PROXY_BODY=false
|
||||
ENV REDACT_PROXY_LOGS=true
|
||||
|
||||
# The toggle. Defaults to root, so this image behaves exactly as before and
|
||||
# the change is non-breaking today. Enabling non-root is planned for v1.0.0
|
||||
# (BREAKING: a bind-mounted DATA_DIR must then be writable by uid 65532 — the
|
||||
# service logs the exact chown command at startup if it can't write). To
|
||||
# enable, either change this default to "aftertouch" (a one-line commit) or
|
||||
# build with --build-arg APP_USER=aftertouch.
|
||||
ARG APP_USER=root
|
||||
USER ${APP_USER}
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/app/soundtouch-service"]
|
||||
|
||||
@@ -1056,6 +1056,8 @@ func createDefaultSettings(ds *datastore.DataStore, config serviceConfig) datast
|
||||
}
|
||||
|
||||
func initDataStore(dataDir string) *datastore.DataStore {
|
||||
warnIfDataDirNotWritable(dataDir)
|
||||
|
||||
ds := datastore.NewDataStore(dataDir)
|
||||
if err := ds.Initialize(); err != nil {
|
||||
log.Printf("Warning: Failed to initialize datastore: %v", err)
|
||||
@@ -1064,6 +1066,48 @@ func initDataStore(dataDir string) *datastore.DataStore {
|
||||
return ds
|
||||
}
|
||||
|
||||
// warnIfDataDirNotWritable probes the data dir and logs an actionable message
|
||||
// when the process can't write to it. The common cause is running the
|
||||
// container as non-root (uid 65532) while a bind-mounted host directory is
|
||||
// owned by someone else; without this the failure would surface later as a
|
||||
// cryptic permission error deep in a save. It only warns: the datastore's own
|
||||
// resilience handles the degraded state.
|
||||
func warnIfDataDirNotWritable(dataDir string) {
|
||||
if dataDir == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||
log.Printf("WARNING: data dir %s cannot be created: %v", sanitizeLog(dataDir), err)
|
||||
logDataDirChownHint(dataDir)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
probe := filepath.Join(dataDir, ".write-probe")
|
||||
if err := os.WriteFile(probe, []byte("ok"), 0o600); err != nil {
|
||||
log.Printf("WARNING: data dir %s is not writable: %v", sanitizeLog(dataDir), err)
|
||||
logDataDirChownHint(dataDir)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
_ = os.Remove(probe)
|
||||
}
|
||||
|
||||
// logDataDirChownHint prints the one-time fix for a non-writable bind-mounted
|
||||
// data dir, using the process's own uid. Skipped where uid is unavailable
|
||||
// (e.g. Windows), where the hint wouldn't apply.
|
||||
func logDataDirChownHint(dataDir string) {
|
||||
uid := os.Getuid()
|
||||
if uid < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf(" The service runs as uid %d. If you bind-mounted a host directory as the data dir, "+
|
||||
"make it writable once: chown -R %d:%d %s", uid, uid, uid, sanitizeLog(dataDir))
|
||||
}
|
||||
|
||||
func initCertificateManager(dataDir, hostname string) *certmanager.CertificateManager {
|
||||
cm := certmanager.NewCertificateManager(filepath.Join(dataDir, "certs"))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user