Files

132 lines
9.8 KiB
Bash
Raw Permalink Normal View History

#!/usr/bin/env bash
# 웹메이커(webmaker) 계정 연동을 위한 읽기 도우미 설치(1.0.8, 요구사항 1) — root 로 실행:
# sudo bash <APP_DIR>/repo/deploy/webmaker-access.sh [<웹메이커 DATA_ROOT>] [--site <site.env 경로>] [--node <node 실행 파일>] [--sudo]
#
# 왜 필요한가
# webmaker 인증 모드는 웹메이커의 사용자 파일(DATA_ROOT/users.json, sites/, modules/, domains.json)을 읽기 전용으로 참조합니다. 그런데 웹메이커는 그 파일을
# 자기 서비스 계정(예: master)만 읽을 수 있게(0600/0700) 쓰므로 AriMail 실행 계정(예: arihakdang)이 직접 읽을 수 없습니다. 웹메이커가 파일을 다시 쓸 때마다
# 권한이 0600 으로 돌아가므로 ACL 로도 풀리지 않습니다. 그래서 "그 계정으로 스냅샷 하나를 읽어 JSON 으로 돌려주는" 도우미를 둡니다.
#
# 하는 일
# 1) server/webmaker-snapshot.mjs 의 사본을 root 소유로 /usr/local/lib/arimail/ 에 둡니다(실행 계정이 바꿀 수 없어야 다른 계정으로 도는 코드가 안전).
# 실행용 node 도 root 소유여야 하므로 /usr/bin/node(22 이상)를 먼저 찾고, 없으면 --node 로 지정한 root 소유 파일을 씁니다.
# 2) systemd 소켓 도우미(기본): /run/arimail-webmaker/<슬러그>.sock 에 접속하면 systemd 가 웹메이커 소유 계정으로 도우미를 띄워 JSON 을 소켓에 씁니다(Accept=yes).
# AriMail 서비스 유닛은 NoNewPrivileges=yes·CapabilityBoundingSet 로 잠겨 있어 sudo 가 동작하지 않으므로 이 방식이 필요합니다. 소켓은 AriMail 실행 계정의 그룹만 접속(0660).
# 3) --sudo: systemd 가 없는 서버용 — 감싸는 스크립트 /usr/local/lib/arimail/webmaker-snapshot 와 sudoers 규칙 한 줄(그 디렉터리 인수 하나만 허용)을 설치합니다.
# 4) 실행 계정으로 한 번 읽어 보고(계정 수 출력), 전체 설정 화면에서 적을 값(디렉터리·읽을 계정)을 안내합니다.
# 게이트웨이 설정·환경 파일·데이터와 웹메이커의 파일은 건드리지 않습니다. 다시 실행해도 안전합니다(같은 내용으로 덮어씀).
set -euo pipefail
[ "$(id -u)" = 0 ] || { echo 'root 로 실행하세요' >&2; exit 1; }
. "$(dirname "$0")/lib.sh"
DATA_ROOT=""; SITE_FILE=""; NODE_OVERRIDE=""; MODE=socket
while [ $# -gt 0 ]; do
case "$1" in
--site) SITE_FILE="$2"; shift 2 ;;
--node) NODE_OVERRIDE="$2"; shift 2 ;;
--sudo) MODE=sudo; shift ;;
-h|--help) sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) [ -z "$DATA_ROOT" ] || { echo "알 수 없는 인수: $1" >&2; exit 1; }; DATA_ROOT="$1"; shift ;;
esac
done
if [ -z "$SITE_FILE" ]; then APP_GUESS="$(cd "$DEPLOY_DIR/../.." && pwd)"; if [ -f "$APP_GUESS/site.env" ]; then SITE_FILE="$APP_GUESS/site.env"; else SITE_FILE="$DEPLOY_DIR/sites/${SITE:-arihakdang}.env"; fi; fi
load_site "$SITE_FILE"
# 데이터 디렉터리: 인수 → webmaker 서비스의 환경 변수(DATA_ROOT) → /data/webmaker
if [ -z "$DATA_ROOT" ]; then
DATA_ROOT="$(systemctl show -p Environment webmaker 2>/dev/null | tr ' ' '\n' | sed -n 's/^DATA_ROOT=//p' | head -1 || true)"
[ -n "$DATA_ROOT" ] || DATA_ROOT=/data/webmaker
fi
DATA_ROOT="${DATA_ROOT%/}"
# sudoers·유닛 파일에 그대로 적히므로 경로에 허용하는 글자를 제한합니다(공백·와일드카드·쉼표가 들어가면 규칙이 깨지거나 넓어짐).
printf '%s' "$DATA_ROOT" | grep -Eq '^/[A-Za-z0-9._/-]+$' || { echo "데이터 디렉터리는 영문·숫자·._- 로 된 절대 경로여야 합니다: $DATA_ROOT" >&2; exit 1; }
[ -f "$DATA_ROOT/users.json" ] || { echo "웹메이커 사용자 파일이 없습니다: $DATA_ROOT/users.json (DATA_ROOT 를 확인하세요)" >&2; exit 1; }
OWNER="$(stat -c %U "$DATA_ROOT/users.json")"
id "$APP_USER" >/dev/null 2>&1 || { echo "AriMail 실행 계정이 없습니다: $APP_USER (사이트 설정 $SITE_FILE)" >&2; exit 1; }
APP_GROUP="$(id -gn "$APP_USER")"
# 실행용 node: root 소유 + 22 이상. /usr/bin/node → --node 지정 → 사이트의 NODE_BIN(루트 소유일 때만).
pick_node() {
local cand; for cand in "$NODE_OVERRIDE" /usr/bin/node "$NODE_BIN"; do
[ -n "$cand" ] && [ -x "$cand" ] || continue
[ "$(stat -c %U "$cand")" = root ] || { echo " (건너뜀) root 소유가 아닌 node: $cand" >&2; continue; }
local major; major="$("$cand" -p 'process.versions.node.split(".")[0]' 2>/dev/null || echo 0)"
[ "$major" -ge 22 ] 2>/dev/null || { echo " (건너뜀) node 22 미만: $cand" >&2; continue; }
printf '%s' "$cand"; return 0
done; return 1
}
NODE_EXEC="$(pick_node)" || { echo "root 소유의 node 22 이상을 찾지 못했습니다. apt 로 설치하거나 --node <경로> 로 지정하세요(실행 계정이 바꿀 수 있는 파일은 쓰지 않습니다)." >&2; exit 1; }
LIB=/usr/local/lib/arimail
# 슬러그·소켓 경로: server/webmaker.mjs 의 webmakerSocketPath 와 같은 규칙(/data/webmaker → data-webmaker).
SLUG="$(printf '%s' "$DATA_ROOT" | sed 's#^/*##; s#[^A-Za-z0-9]#-#g')"; SOCK_DIR=/run/arimail-webmaker; SOCK="$SOCK_DIR/$SLUG.sock"
echo "▶ 웹메이커 읽기 도우미 설치($MODE): 데이터 $DATA_ROOT (소유 $OWNER) · AriMail 계정 $APP_USER($APP_GROUP) · node $NODE_EXEC"
install -d -o root -g root -m 755 "$LIB"
install -o root -g root -m 644 "$DEPLOY_DIR/../server/webmaker-snapshot.mjs" "$LIB/webmaker-snapshot.mjs"
[ "$MODE" = socket ] && ! command -v systemctl >/dev/null 2>&1 && { echo " systemd 가 없어 sudo 방식으로 설치합니다"; MODE=sudo; }
if [ "$MODE" = socket ]; then
UNIT="arimail-webmaker-$SLUG"
install -d -o root -g root -m 755 "$SOCK_DIR"
printf 'd %s 0755 root root -\n' "$SOCK_DIR" > /etc/tmpfiles.d/arimail-webmaker.conf
cat > "/etc/systemd/system/$UNIT.socket" <<UNIT1
[Unit]
Description=AriMail webmaker account snapshot socket ($DATA_ROOT)
# deploy/webmaker-access.sh 가 설치. AriMail(LOGIN_MODE=webmaker, 읽을 계정 $OWNER)이 접속하면 아래 서비스가 $OWNER 로 스냅샷 JSON 을 써 줍니다.
[Socket]
ListenStream=$SOCK
SocketUser=$OWNER
SocketGroup=$APP_GROUP
SocketMode=0660
Accept=yes
MaxConnections=16
[Install]
WantedBy=sockets.target
UNIT1
cat > "/etc/systemd/system/$UNIT@.service" <<UNIT2
[Unit]
Description=AriMail webmaker account snapshot ($DATA_ROOT, per connection)
CollectMode=inactive-or-failed
[Service]
User=$OWNER
ExecStart=$NODE_EXEC $LIB/webmaker-snapshot.mjs $DATA_ROOT
StandardInput=socket
StandardOutput=socket
StandardError=journal
NoNewPrivileges=yes
ProtectSystem=strict
PrivateTmp=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
UNIT2
chmod 644 "/etc/systemd/system/$UNIT.socket" "/etc/systemd/system/$UNIT@.service"
systemctl daemon-reload; systemctl enable --now "$UNIT.socket" >/dev/null 2>&1 || systemctl restart "$UNIT.socket"
systemctl restart "$UNIT.socket"
echo " 소켓: $SOCK ($UNIT.socket, 서비스 $UNIT@.service → $OWNER)"
# 실행 계정으로 실제 접속(계정 수만 출력)
READ_CMD="sudo -u $APP_USER $NODE_EXEC -e"
JS='const s=require("net").connect(process.argv[1]);let b="";s.setEncoding("utf8");s.on("data",c=>b+=c);s.on("end",()=>{const d=JSON.parse(b);console.log(`회원 ${Object.keys(d.users).length}명 · 사이트 ${Object.keys(d.sites).length}개 · 도메인 연결 ${Object.keys(d.domainSites).length}개`);});s.on("error",e=>{console.error(e.message);process.exit(1);});'
if OUT="$($READ_CMD "$JS" "$SOCK" 2>&1)"; then echo "✔ 확인: $APP_USER 계정이 소켓으로 스냅샷을 읽었습니다 — $OUT"
else echo "✖ 실행 계정으로 소켓 읽기에 실패했습니다: $OUT (journalctl -u '$UNIT@*' 확인)" >&2; exit 1; fi
else
cat > "$LIB/webmaker-snapshot" <<WRAP
#!/bin/sh
# AriMail 웹메이커 계정 스냅샷 도우미(deploy/webmaker-access.sh 가 설치). 첫 인수(데이터 디렉터리)만 넘기고 나머지는 무시합니다.
exec "$NODE_EXEC" "$LIB/webmaker-snapshot.mjs" "\$1"
WRAP
chmod 755 "$LIB/webmaker-snapshot"; chown root:root "$LIB/webmaker-snapshot"
# sudoers.d 는 이름에 점이 든 파일을 무시하므로 점을 빼고, 임시 파일을 visudo 로 검사한 뒤에 설치합니다(틀린 파일이 들어가면 서버의 sudo 전체가 막힘).
RULE="/etc/sudoers.d/$(printf '%s' "$APP_NAME" | tr -c 'A-Za-z0-9_-' '-')-webmaker"
TMP_RULE="$(mktemp)"; printf '%s ALL=(%s) NOPASSWD: %s %s\n' "$APP_USER" "$OWNER" "$LIB/webmaker-snapshot" "$DATA_ROOT" > "$TMP_RULE"
visudo -cf "$TMP_RULE" >/dev/null || { rm -f "$TMP_RULE"; echo "✖ sudoers 규칙 검사 실패(계정 이름·경로 확인)" >&2; exit 1; }
install -o root -g root -m 440 "$TMP_RULE" "$RULE"; rm -f "$TMP_RULE"
echo " sudoers: $RULE (주의: AriMail 서비스 유닛에 NoNewPrivileges=yes 가 있으면 sudo 가 동작하지 않습니다 — systemd 서버에서는 소켓 방식을 쓰세요)"
if OUT="$(sudo -u "$APP_USER" sudo -n -u "$OWNER" "$LIB/webmaker-snapshot" "$DATA_ROOT" 2>&1)"; then
N="$(printf '%s' "$OUT" | "$NODE_EXEC" -e 'let s="";process.stdin.on("data",(c)=>s+=c).on("end",()=>{const d=JSON.parse(s);console.log(`회원 ${Object.keys(d.users).length}명 · 사이트 ${Object.keys(d.sites).length}개 · 도메인 연결 ${Object.keys(d.domainSites).length}개`);});')"
echo "✔ 확인: $APP_USER 계정이 $OWNER 로 스냅샷을 읽었습니다 — $N"
else echo "✖ 실행 계정으로 읽기에 실패했습니다: $(printf '%s' "$OUT" | tail -1)" >&2; exit 1; fi
fi
cat <<EOF2
다음 단계: 전체 설정 화면(https://$MAIL_HOST:8791/?token=…)의 도메인 카드에서 도메인을 추가·수정하며
로그인 방식 = 웹메이커(webmaker) 계정과 연동, 웹메이커 데이터 디렉터리 = $DATA_ROOT, 파일을 읽을 계정 = $OWNER
로 저장하세요(기본 도메인이면 환경 파일 $ETC_DIR/gateway.env 에 LOGIN_MODE=webmaker, WEBMAKER_DIR=$DATA_ROOT, WEBMAKER_USER=$OWNER 를 적고 재시작).
EOF2