Sidekiq runit 스크립트가 너무 취약합니다

팀 여러분께,

공식 Docker/runit 설정에서 Sidekiq(따라서 AI/백그라운드 작업)를 재빌드나 업그레이드 없이도 조용히 종료시키는 실패 모드를 보고드립니다.

환경

  • 공식 Discourse Docker 설치 (표준 컨테이너 + runit 서비스).
  • 문제가 시작되기 직전에 재빌드/업그레이드를 수행하지 않았습니다.
  • Discourse AI 플러그인은 활성화되어 있었지만, AI가 응답을 중단했습니다.

증상

  • 관리자 UI에서는 AI가 활성화되어 보이지만, AI 응답이 표시되지 않습니다.
  • 백그라운드 작업(AI/임베딩/자동 응답)이 막혀 있는 것으로 보입니다.
  • sv status sidekiq 명령을 실행하면 Sidekiq이 시작 직후 반복적으로 죽고 있는 것을 확인할 수 있습니다:
down: sidekiq: 1s, normally up, want up
  • 수동으로 Sidekiq를 시작하면 정상 작동하므로, 애플리케이션 자체에는 문제가 없습니다:
bundle exec sidekiq -C config/sidekiq.yml
# 계속 실행되며, Redis에 연결되고 작업을 처리합니다

발견한 내용

기본 runit 스크립트는 다음과 같았습니다:

exec chpst -u discourse:www-data \
  bash -lc 'cd /var/www/discourse && ... bundle exec sidekiq -e production -L log/sidekiq.log'

두 가지 취약점이 있습니다:

  1. 기본 그룹 www-data 제 컨테이너에서 일반적인 쓰기 가능 경로는 discourse:discourse 소유입니다. tmp/pids 또는 공유 경로에 소유권 불일치가 발생하면, 수동으로 discourse 사용자로 시작할 때는 정상 작동하더라도 www-data로 실행될 때 Sidekiq이 부팅 중에 종료될 수 있습니다.
  2. 강제된 -L log/sidekiq.log 공유 로그에 쓰기 로그 경로는 /shared/log/rails/sidekiq.log으로 향하는 심볼릭 링크입니다. 해당 파일/디렉터리의 소유권/권한이 다르게 재생성되면, Sidekiq은 유용한 로그를 생성하기도 전에 즉시 종료될 수 있습니다.

관련 트리거: 매일 실패하는 logrotate

별개로, logrotate가 매일 다음과 같은 오류로 실패하고 있었습니다:

error: skipping "...log" because parent directory has insecure permissions
Set "su" directive in config file ...

원인은 표준 Debian/Ubuntu 권한 설정이었습니다:

  • /var/log는 root:adm 소유이며 0775(그룹 쓰기 가능) 권한을 가지고 있습니다.
  • 전역 su 지시문이 설정되지 않으면 logrotate는 로테이션을 거부합니다. 이는 업스트림의 예상된 동작입니다.

매일 logrotate 작업이 실패하던 시점에 /shared/log/rails/ 하위 파일들( sidekiq.log 포함)이 재생성되었으며, 이는 강제된 -L 로깅과 상호작용하여 Sidekiq의 “1s 크래시” 루프에 기여한 것으로 보입니다.

수정 방법 (재빌드 불필요)

  1. logrotate를 수정하여 실패 상태일 때 공유 로그를 건드리지 않도록 합니다 전역 su 지시문을 추가합니다:
# /etc/logrotate.conf (상단)
su root adm

이후 logrotate -v는 0으로 종료되며 더 이상 불안전한 부모 디렉터리 권한을 보고하지 않습니다.

  1. Sidekiq runit 스크립트를 더 견고한 기본값으로 교체합니다 discourse:discourse와 표준 sidekiq.yml로 전환하고, -L log/sidekiq.log를 강제하지 않으면 Sidekiq이 안정적으로 작동합니다:
#!/bin/bash
exec 2>&1
cd /var/www/discourse

mkdir -p tmp/pids
chown discourse:discourse tmp/pids || true

exec chpst -u discourse:discourse \
  bash -lc 'cd /var/www/discourse && rm -f tmp/pids/sidekiq*.pid; exec bundle exec sidekiq -C config/sidekiq.yml'

이 수정 이후:

  • sv status sidekiq가 run 상태로 유지됩니다.
  • AI/백그라운드 작업이 재개됩니다.

요청 / 제안

공식 Docker/runit Sidekiq 서비스를 기본적으로 더 견고하게 만들 수 있을까요?

예를 들어:

  • Sidekiq을 discourse:discourse로 실행 (컨테이너 내부의 일반적인 소유권과 일치하도록).
  • bundle exec sidekiq -C config/sidekiq.yml 사용을 권장.
  • -L log/sidekiq.log를 통해 공유 로그 파일을 강제하지 않거나, logrotate/공유 볼륨 권한 불일치에 대해 견고하게 처리.

심지어 문서에 간단한 노트(“Sidekiq이 down: 1s를 표시하지만 수동 시작은 정상 작동하는 경우, /etc/service/sidekiq/run을 확인하고 강제된 공유 로깅을 피하세요”)만 있어도 셀프 호스터들에게 큰 도움이 될 것입니다.

필요하다면 추가 로그를 제공할 수 있습니다. 감사합니다!

그 코드를 어디서 찾았나요? Sidekiq은 메모리를 절약하기 위해 unicorn 마스터를 통해 시작됩니다. discourse_docker에서 해당 코드를 전혀 찾을 수 없습니다. 아마도 아주 오래된 설정을 사용 중인 것 같습니다.

안녕하세요 — 공식 Docker 컨테이너의 런타임 사실에 기반하여 이 문제를 엄격하게 다시 정리해 드리겠습니다.

실행 중인 컨테이너에서 확인된 내용 (사실)

이것은 runit이 포함된 공식 Docker 설치 환경입니다 (표준 /var/discourse 런처 워크플로우; 사고 직전에 재빌드가 이루어지지 않음). 컨테이너 내부에서 확인된 사항은 다음과 같습니다:

  1. runit Sidekiq 서비스가 존재하며, 이 서비스가 감독(supervise)되고 있습니다
ls -l /etc/service/sidekiq/run
sv status sidekiq

사고 당시 출력 결과:

down: sidekiq: 1s, normally up, want up
  1. 수동으로 Sidekiq를 시작하면 정상 작동합니다
cd /var/www/discourse
sudo -u discourse bundle exec sidekiq -C config/sidekiq.yml

이 방식으로는 프로세스가 유지되며, Redis에 연결되고 작업을 처리합니다.

  1. /etc/service/sidekiq/run 파일만 패치해도 (재빌드 없이) 크래시 루프가 즉시 해결됩니다.
    /etc/service/sidekiq/run을 다음과 같이 대체했습니다:
#!/bin/bash
exec 2>&1
cd /var/www/discourse
mkdir -p tmp/pids
chown discourse:discourse tmp/pids || true
exec chpst -u discourse:discourse \
  bash -lc 'cd /var/www/discourse && rm -f tmp/pids/sidekiq*.pid; exec bundle exec sidekiq -C config/sidekiq.yml'

그 후:

sv status sidekiq
run: sidekiq: (pid <PID>) <SECONDS>s

즉, 이 이미지에서 Sidekiq는 Unicorn 마스터를 통해 시작되지 않습니다. 런타임 스크립트가 크래시 루프에 빠질 수 있는 runit 서비스입니다.

정확한 코드를

discourse_docker

레포지토리에서 볼 수 없는 이유

**/etc/service/sidekiq/run은 이미지 빌드/부팅 동안 생성되거나 주입되는 런타임 산출물(runtime artifact)**이기 때문에, discourse_docker에 문자 그대로의 파일로 존재하지 않을 수 있다는 점에는 동의합니다. 그러나 위와 같이, 이 공식 이미지에서 실제로 감독되는 활성 서비스임은 분명합니다.

취약성을 유발한 요인 (사실 + 최소한의 추론)

  • 표준 Debian 권한(/var/log = root:adm 0775)으로 인해 logrotate가 매일 실패하는 것도 관찰되었습니다. 전역 su root adm를 추가하기 전까지는 logrotate가 로테이션을 거부했습니다.
  • logrotate가 실패할 때 /shared/log/rails/ 하위의 파일들, sidekiq.log를 포함하여 재생성되었습니다.
  • 이 이미지의 기본 runit 스크립트는 discourse:www-data를 사용했으며, -L log/sidekiq.log를 /shared/log로 강제했습니다. 이로 인해 Sidekiq는 공유 볼륨 권한 드리프트(perms drift)에 매우 민감해지며, 유용한 로그가 기록되기 전에 즉시 종료될 수 있습니다.

요청 / 제안

위 내용을 고려할 때, 기본 Docker/runit Sidekiq 서비스를 강화하는 방안을 검토해 주실 수 있을까요?

제안하는 기본 설정:

  • discourse:discourse로 실행 (컨테이너 내부의 일반적인 소유권과 일치),
  • bundle exec sidekiq -C config/sidekiq.yml을 통해 시작,
  • 공유 -L log/sidekiq.log를 강제하지 않거나 (또는 이를 견고하게 처리).

이렇게 하면 모든 백그라운드/AI 작업을 중단시키는 조용한 down: 1s 크래시 루프를 방지할 수 있습니다.

지시해 주시는 브랜치/커밋을 테스트해 드릴 수 있습니다.

다시 말하지만… 이미지가 어디서 가져온 건지 혼란스럽습니다:

image

이것은 공식 이미지입니다.

공식 discourse docker에서 'sidekiq’라는 단어로 검색한 결과입니다.

https://github.com/search?q=repo%3Adiscourse%2Fdiscourse_docker%20sidekiq&type=code

검색 결과가 3건 있습니다… runit 유닛에 대한 내용은 없습니다. unicorn을 통해 관리됩니다.

안녕하세요 — 감사합니다. 그 스크린샷 덕분에 레이아웃이 명확해졌습니다.

현재 공식 이미지에서 Sidekiq가 별도의 runit 서비스가 아님을 동의합니다 (/etc/service/sidekiq/ 디렉터리가 없음). 이는 unicorn runit 서비스의 시작 체인에서 시작되며, 이는 사용자의 /etc/service 목록과 일치합니다.

제 보고서는 그것이 독립적인 runit 유닛에 있든 unicorn/run 안에 있든 상관없이, 해당 Sidekiq 시작 경로의 런타임 실패 모드에 관한 것입니다:

제 VPS의 런타임에서 확인된 사실 (공식 Docker, 사고 직전에 재빌드/업그레이드 없음):

  1. 백그라운드 작업이 중단되고 AI 응답이 중단되었습니다.

  2. 컨테이너의 슈퍼바이저/시작 체인에 의해 시작될 때 Sidekiq은 즉시 크래시 루프(다운: 1초)에 진입했습니다.

  3. discourse 사용자로 bundle exec sidekiq -C config/sidekiq.yml 명령을 통해 수동으로 시작했을 때 프로세스가 유지되고 작업이 처리되었으므로, 앱과 Redis는 정상 상태였습니다.

  4. 같은 시점에 logrotate 실패로 인해 /shared/log/rails/sidekiq.log(및 관련 경로)가 다른 권한으로 다시 생성되었습니다. Sidekiq 시작 명령을 안정화한 후(실행: discourse:discourse, 사용: sidekiq.yml, 공유 강제 -L sidekiq.log 회피`), 크래시 루프가 즉시 중단되었습니다.

따라서 이 이미지에서 /etc/service/sidekiq/run 파일이 실제로 존재하지 않을 수 있다는 점에는 동의합니다. 그러나 unicorn runit 서비스에 내장된 Sidekiq 시작 단계는 공유 볼륨 권한/logrotate 변동에 취약하여, 재빌드 없이도 Sidekiq을 조용히 종료시킬 수 있습니다. 이것이 핵심 문제입니다.

제안: 공식 unicorn runit 스크립트(또는 해당 스크립트가 생성되는 위치)에서 Sidekiq 시작 부분을 강화하는 것을 고려해 주십시오:

  • discourse:discourse로 Sidekiq 실행,

  • bundle exec sidekiq -C config/sidekiq.yml 선호,

  • 공유 -L log/sidekiq.log 강제 회피(또는 내구성 확보).