diff --git a/account.example.json b/account.example.json new file mode 100644 index 0000000..198bc79 --- /dev/null +++ b/account.example.json @@ -0,0 +1,3 @@ +[ + { "id": "admin", "password": "여기에-비밀번호를-넣으세요" } +] diff --git a/src/server/app.ts b/src/server/app.ts index 01ed42c..367ded7 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -6,7 +6,8 @@ import fsp from 'node:fs/promises' import crypto from 'node:crypto' import { manifestRootPath, manifestDirPath, manifestTermsDirPath, - fileDirPath, viewsDirPath, publicDirPath, projectRoot + fileDirPath, viewsDirPath, publicDirPath, projectRoot, + accountFilePath, accountLocalFilePath } from '../shared/paths.js' import { ensurePackTermsDir, isPublicTermsFile, listTermsWithLabels, loadPackDefinition @@ -27,7 +28,24 @@ const app = express() app.set('view engine', 'ejs') app.set('views', viewsDirPath) -app.set('trust proxy', 1) +// 리버스 프록시 뒤일 때만 켠다. 항상 켜두면 직접 노출 시 X-Forwarded-For 조작으로 +// req.ip 를 위조해 로그인 rate limit 을 우회할 수 있다. 프록시 뒤라면 TRUST_PROXY=true. +app.set('trust proxy', process.env.TRUST_PROXY === 'true' ? 1 : false) + +// 추적되는 account.json(과거 평문 노출)을 gitignore 된 account.local.json 으로 시드한다. +// 로컬 파일이 이미 있으면 건드리지 않음. 이후 계정 쓰기/자동 해시 업그레이드는 로컬 +// 파일에만 반영되어, 재배포로 account.json 을 추적 해제해도 로그인이 유지된다. +function seedLocalAccounts(): void { + try { + if (fs.existsSync(accountLocalFilePath)) return + if (!fs.existsSync(accountFilePath)) return + fs.copyFileSync(accountFilePath, accountLocalFilePath) + fs.chmodSync(accountLocalFilePath, 0o600) + } catch { + // 실패해도 readAccounts 가 account.json 으로 폴백하므로 치명적이지 않음. + } +} +seedLocalAccounts() app.use(express.urlencoded({ extended: true })) app.use(express.json()) diff --git a/src/shared/store.ts b/src/shared/store.ts index 8c4c8bf..7b2363f 100644 --- a/src/shared/store.ts +++ b/src/shared/store.ts @@ -795,6 +795,7 @@ export async function readAccounts(): Promise { } // 운영 계정 저장은 항상 gitignore 된 account.local.json 에만 한다(추적 파일 오염 방지). +// 비밀번호(해시) 파일이므로 소유자만 읽기/쓰기(0o600). export async function writeAccounts(accounts: AccountEntry[]): Promise { - await fsp.writeFile(accountLocalFilePath, `${JSON.stringify(accounts, null, 2)}\n`, 'utf8') + await fsp.writeFile(accountLocalFilePath, `${JSON.stringify(accounts, null, 2)}\n`, { mode: 0o600 }) }