Gosu คืออะไร ทำไม Open source image หลาย ๆ ตัวถึงติดมาด้วย ??
เหตุเกิดจากผมได้ทำไล่ Scan image ต่าง ๆ เพื่อเช็คว่า image นั้นมี CVE ของ OS หรือ Application package ไหนติดมาด้วยมั้ย และเจอว่าทำไมทั้ง redis, postgresql มันมี binary gosu ติดมาด้วยหมดเลยแหะ
ก็เลยไปหาว่าเจ้า gosu นี่จริง ๆ แล้วมันมีไว้ทำอะไรกันแน่
ตัวอย่างผล scan redis:7.4.7-alpine
และของ postgres:17.4-alpine
เลยไปเจอต้นตออยู่ที่ https://github.com/tianon/gosu อธิบายการทำงานของมันคือ จะเป็นตัวที่ช่วยในการ step down from root to non-privileged user ตอนที่ container startup ขึ้นมา เพื่อให้สิทธิ์ของ app ที่รันอยู่ไม่รันด้วยสิทธิ์ของ root นั้นเอง
ตัวอย่างจาก redis จะเริ่มต้นด้วยสิทธิ root และไปเรียก startup script (ENTRYPOINT) ก็คือ /usr/local/bin/docker-entrypoint.sh ซึ่งข้างในจะมีการเรียกใช้ gosu เพื่อรัน redis-server ด้วย user redis
/usr/local/bin # cat docker-entrypoint.sh
#!/bin/sh
set -e
# first arg is `-f` or `--some-option`
# or first arg is `something.conf`
if [ "${1#-}" != "$1" ] || [ "${1%.conf}" != "$1" ]; then
set -- redis-server "$@"
fi
# allow the container to be started with `--user`
if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
find . \! -user redis -exec chown redis '{}' +
exec gosu redis "$0" "$@"
fi
# set an appropriate umask (if one isn't set already)
# - https://github.com/docker-library/redis/issues/305
# - https://github.com/redis/redis/blob/bb875603fb7ff3f9d19aad906bd45d7db98d9a39/utils/systemd-redis_server.service#L37
um="$(umask)"
if [ "$um" = '0022' ]; then
umask 0077
fi
exec "$@"
แล้วทำไมเราต้องใช้ gosu ??? ทำไมไม่ set USER ใน Dockerfile เพื่อ least privilege ตั้งแต่ตอน Build เลยหละ
อันที่จริงเราสามารถกำหนด USER ที่จะใช้รัน container ตั้งแต่ตอน build ได้เลยใน Dockerfile นี่นา เช่นแบบนี้
FROM alpine:3.23.3
RUN adduser -u 1337 -D newuser
RUN mkdir /container-data && chown newuser /container-data
USER newuser
จากนั้นก็ลองรันเพื่อทดสอบ application และทดสอบเขียนไฟล์ก็ปกติดี ด้วยสิทธิ์ของ newuser

แต่ในกรณีของ redis หรือ postgres ซึ่งเป็น database หรือ application บางตัวที่ต้องการทำ persistent storage นั้นจำเป็นต้อง mapping volume ออกมาเพื่อเก็บ data ใน host แทน container เพราะว่า container อาจจะโดนลบออกได้เสมอทำให้ถ้าไม่ทำ persistent storage ข้อมูลก็จะหายไปพร้อมกับ container แต่ถ้าเราเก็บ data ไว้ใน host เวลาที่เรา start container ขึ้นมาใหม่เราก็จะสามารถใช้ data เดิมต่อจากเดิมได้เลยผ่าน mapping volume ที่เคยเก็บไว้
อ่าว!! ทำไมอยู่ดี ๆ ถึงเขียนไฟล์ไม่ได้หละ แล้วทำไมจู่ ๆ owner ของ /container-data กลายเป็น root ??
เนื่องจากเมื่อเราทำการ mapping volume ระหว่าง host กับ container ซึ่งการ mount ใน Linux จะทำการ Overwrite สิทธิ์ต่าง ๆ โดยจะอิงจาก Source ของ data เอง (เครื่อง host) ทำให้ directory นั้นกลายเป็นสิทธิ์ root (หรือสิทธิ์อื่น ๆ ตามเครื่อง host) ทำให้ application ไม่สามารถเขียนด้วยสิทธิ์ของ newuser ได้อีกต่อไป
ซึ่งในจุดนี้เองที่ gosu จะมาช่วยในการให้ container สามารถรันด้วยสิทธิ์ของ root ก่อนในช่วงแรก เพื่อจัดการ file permission ต่าง ๆ ให้ Application (Non-privileged user) สามารถเขียน data ไปยัง file นั้น ๆ ได้

สรุปก็คือ gosu สามารถช่วยจัดการ configuration ต่าง ๆ เช่น file permission ด้วยสิทธิ์ root ในช่วงแรก และสลับเป็น non-privilege user เพื่อรัน application หลังจากที่ config ทุกอย่างแล้ว ซึ่งจะช่วยแก้ไขปัญหาต่าง ๆ เช่น เวลาทำ persistent storage แล้วโดน mounting overwrite permission ทำให้ไม่สามารถเขียนไฟล์ได้
เพิ่มเติมนอกจาก gosu จะช่วยให้สามารถรันคำสั่งต่าง ๆ ด้วยสิทธิ root ในช่วงแรก และสลับเป็น non-privilege สำหรับ application ในช่วงหลังแล้ว มันยังช่วยให้เรื่อง signal handling ของ application ที่รันในช่วงหลังได้ PID 1 ซึ่งจะเป็น process ที่ได้ signal ต่าง ๆ เช่น เมื่อสั่ง docker stop แล้ว application นั้นก็จะได้รับ signal นั้นตรง ๆ เพื่อทำ graceful shutdown อีกด้วย
