보안된 Linux 서버 설정 및 애플리케이션 배포

발행: (2025년 12월 18일 오후 11:00 GMT+9)
13 분 소요
원문: Dev.to

Source: Dev.to

애플리케이션을 배포하는 것은 쉽습니다.
하지만 하나의 애플리케이션이 손상되었을 때 전체 서버가 무너지지 않도록 안전하게 실행하려면 규율과 구조가 필요합니다.

이 가이드는 우리가 정확히 따르는 절차를 문서화한 것입니다:

  • 새 Linux 서버 준비
  • 데이터베이스 및 애플리케이션 배포
  • 시스템을 안전하게, 격리된, 그리고 유지보수 가능하게 유지

이 방법은 전투 검증을 거쳤으며 실제 프로덕션 서버에 적합합니다.

이 설정이 적용되는 경우

카테고리예시
Node.js 백엔드NestJS, Express, Fastify
Next.js독립 실행 빌드
정적 프런트엔드React, Vite
데이터베이스 (Docker)PostgreSQL, MongoDB, Redis
리버스 프록시Caddy, Nginx (HTTPS)

핵심 원칙

  • Root는 앱 런타임이 아닙니다 – 서비스를 root 권한으로 실행하지 마세요.
  • 한 앱 = 하나의 서비스 사용자 – 각 앱은 자체 저권한 Linux 사용자를 가집니다.
  • 인간이 배포하고, 서비스가 실행됩니다 – sudo 접근 권한은 인간에게만 부여됩니다.
  • 데이터베이스는 기본적으로 비공개입니다127.0.0.1에만 바인딩합니다.
  • 리버스 프록시가 유일한 공개 진입점입니다.
  • 하나의 앱이 결국 침해될 것이라고 가정합니다 – 격리를 위해 설계합니다.

우리의 목표는 간단합니다: 하나의 애플리케이션이 해킹당하더라도, 다른 모든 것은 안전하게 유지되어야 합니다.

1. 일반 관리자 사용자 만들기

새 서버에서는 보통 root 계정으로 시작합니다.

# Create a non‑root admin user (example: dev)
adduser dev
usermod -aG sudo dev

왜?

  • Root SSH 접근은 위험합니다.
  • sudo 사용은 감사 가능합니다.
  • 사고 발생 시 복구가 더 쉽습니다.

2. SSH 키 인증 설정

# Generate a modern SSH key (ed25519)
ssh-keygen -t ed25519

공개 키를 서버에 복사합니다:

mkdir -p /home/dev/.ssh
nano /home/dev/.ssh/authorized_keys   # paste your public key here

권한 수정:

chown -R dev:dev /home/dev/.ssh
chmod 700 /home/dev/.ssh
chmod 600 /home/dev/.ssh/authorized_keys

sshd_config 강화

sudo nano /etc/ssh/sshd_config

다음 라인이 존재하는지 확인하십시오 (없으면 추가하세요):

PermitRootLogin no
PubkeyAuthentication yes

# Disable password login
Match all
    PasswordAuthentication no

SSH를 안전하게 다시 로드합니다:

sudo systemctl reload ssh

3. 방화벽 설정 (UFW)

# Allow only what’s required
sudo ufw allow OpenSSH      # port 22
sudo ufw allow 80           # HTTP
sudo ufw allow 443          # HTTPS
sudo ufw enable
sudo ufw status verbose

결과: 포트 22, 80, 443만 공개적으로 접근 가능합니다.

4. Install Docker (Official Repository)

Never use the docker.io package from the default Ubuntu repo.

# Follow Docker’s official installation guide for your distro.
# After installation, add the admin user to the docker group:
sudo usermod -aG docker dev

Re‑login and verify:

docker ps

Important Rules

  • Never add service users to the docker group.
  • Docker runs with root‑equivalent privileges, so only the admin user (dev) should be allowed to use it.

5. 데이터베이스를 안전하게 배포하기 (Docker)

예시: PostgreSQL (보안)

docker run -d \
  --name postgres \
  --restart unless-stopped \
  -e POSTGRES_USER=appuser \
  -e POSTGRES_PASSWORD=STRONG_PASSWORD \
  -e POSTGRES_DB=appdb \
  -v pgdata:/var/lib/postgresql \
  -p 127.0.0.1:5432:5432 \
  postgres:18

바인딩 확인:

ss -tulpn | grep 5432
# Expected output: 127.0.0.1:5432

127.0.0.1에 바인드할까?

Docker는 공개된 포트에 대해 UFW 규칙을 무시합니다. 자체 iptables 규칙을 삽입하기 때문에 0.0.0.0에 바인드된 컨테이너는 UFW가 포트를 차단하더라도 외부에서 접근 가능하게 됩니다. 127.0.0.1에 바인드하면 데이터베이스가 호스트 자체에서만 접근 가능하도록 보장됩니다.

로컬 머신에서 접근하기 (SSH 터널)

ssh -N -L 5432:127.0.0.1:5432 dev@SERVER_IP

이제 로컬에서 연결:

HostPort
127.0.0.15432

🔐 암호화된, 개인적인, 안전한.

6. 개인 저장소 접근 관리 (배포 키)

각 개인 저장소마다 관리자 사용자로 전용 SSH 배포 키를 생성합니다:

ssh-keygen -t ed25519 -C "deploy-myapp" -f ~/.ssh/id_ed25519_myapp
  • 공개 키(id_ed25519_myapp.pub)를 GitHub에 Deploy key(읽기 전용)로 추가합니다.
  • SSH 별칭을 사용하여 클론합니다(HTTPS 사용 금지).

7. 각 앱에 대한 잠금된 서비스 사용자 생성

sudo adduser \
  --system \
  --no-create-home \
  --group \
  --shell /usr/sbin/nologin \
  svc-myapp

이 사용자의 특성

  • SSH 접속 불가.
  • 쉘이 없음.
  • sudo 권한 없음.
  • 오직 해당 앱 디렉터리만 소유.

애플리케이션 배포

# Switch to the admin user (dev) and clone the repo
cd /var/apps
git clone git@github.com-myapp:org/repo.git
cd repo

# Install dependencies and build
npm ci
npm run build
npm prune --production

인간이 빌드합니다. 프로덕션에서는 환경 변수는 절대 Git에 커밋되지 않습니다.

8. 런타임 환경 변수 안전하게 저장하기

시스템이 관리하는 env 파일을 생성합니다 (root 소유):

# /etc/systemd/system/myapp.env
# Example content:
# DATABASE_URL=postgres://appuser:STRONG_PASSWORD@127.0.0.1:5432/appdb
# OTHER_SECRET=...
  • 이 파일은 실행 시 systemd에 의해 로드됩니다.
  • Next.js 프로젝트가 NEXT_PUBLIC_ 접두사가 붙은 변수를 사용한다면, 빌드 시점에 해당 변수가 존재해야 합니다. 컴파일러가 변수를 코드에 삽입하기 때문입니다.

공개 변수와 함께 빌드하기 (필요한 경우)

sudo -E bash -c '
  set -a
  source /etc/systemd/system/myapp.env
  set +a
  npm run build
'

NEXT_PUBLIC_* 변수가 없다면, 이 단계는 필요하지 않습니다 – 런타임 주입을 systemd가 담당하면 충분합니다.

9. Standalone Next.js 빌드 준비

Next.js를 스탠드얼론 앱으로 빌드할 때는 정적 자산을 수동으로 복사합니다:

# `npm run build` (또는 `next build`) 실행 후
mkdir -p .next/standalone/.next
cp -r .next/static .next/standalone/.next/
cp -r public .next/standalone/

왜?
스탠드얼론 출력물에는 서버 코드만 포함되고 정적 파일(_next/static, public/)은 자동으로 제외됩니다. 이를 복사하지 않으면 앱은 실행되지만 자산 요청이 404 오류를 반환합니다.

10. 애플리케이션 디렉터리 권한 설정

sudo chown -R svc-myapp:svc-myapp /var/apps/myapp
sudo chmod -R o-rwx /var/apps/myapp
# Even the admin user (dev) now gets permission denied – intentional.

11. Systemd 서비스 만들기

# /etc/systemd/system/myapp.service
[Unit]
Description=My Application
After=network.target

[Service]
User=svc-myapp
Group=svc-myapp
WorkingDirectory=/var/apps/myapp
EnvironmentFile=/etc/systemd/system/myapp.env
ExecStart=/usr/bin/node dist/main.js
Restart=always
RestartSec=3

# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/apps/myapp
PrivateTmp=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictNamespaces=true
LockPersonality=true
RestrictSUIDSGID=true
CapabilityBoundingSet=
AmbientCapabilities=

서비스를 활성화하고 시작합니다:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
sudo systemctl status myapp.service

12. 요약 체크리스트

단계완료 여부
admin 사용자 (dev) 생성
SSH 키 인증 설정 및 sshd 강화
UFW 설정 (22, 80, 443)
공식 저장소에서 Docker 설치
admin 사용자를 docker 그룹에만 추가
127.0.0.1 바인딩으로 데이터베이스 배포
레포별 배포 키 생성
저권한 서비스 사용자 (svc‑myapp) 생성
앱 클론, 빌드 및 정리
환경 변수 /etc/systemd/system/*.env에 저장
Next.js standalone 빌드 (정적 자산 복사)
앱 파일에 엄격한 권한 설정
강화된 systemd 서비스 생성
방화벽, Docker, 서비스 상태 확인

새 서버마다 이 가이드를 따라 하면 보안이 강화되고 격리된 유지보수 가능한 프로덕션 환경을 구축할 수 있습니다. 하나의 앱이 침해되더라도 전체 호스트가 다운되지 않습니다. 배포 즐겁게 하세요!

Mask=0077

[Install]
WantedBy=multi-user.target

활성화 및 시작 (대안)

sudo systemctl daemon-reload
sudo systemctl enable myapp
sudo systemctl start myapp

Source:

정적 프론트엔드 앱 (React, Vite 등)

정적 앱은 systemd를 통해 실행되지 않습니다. 한 번 빌드된 뒤 리버스 프록시가 직접 제공합니다.

  1. env 파일을 프로젝트 루트에 배치합니다:

    .env.production
  2. 명시적으로 빌드합니다:

    npm run build

    빌드 결과물(HTML, JS, CSS, assets)은 이제 완전한 정적 파일이며, 주입된 값들을 포함합니다.

  3. Caddy에 정적 빌드 폴더에 대한 읽기 전용 권한을 부여합니다:

    sudo chown -R svc-frontend:svc-frontend /var/apps/frontend
    sudo chmod -R 755 /var/apps/frontend/dist

왜 이렇게 하는가

  • Caddy는 정적 파일을 읽기만 하면 되므로 쓰기 권한이 필요 없습니다.
  • 실수나 악의적인 파일 수정으로부터 보호합니다.
  • 정적 앱은 런타임이 없고, 열린 포트가 없으며, 백그라운드 프로세스가 없으므로 공격 표면이 크게 감소합니다.

Caddy가 서버에 대한 유일한 공개 진입점이며, 애플리케이션은 내부 사설 포트(예: 127.0.0.1:5000)에서 실행됩니다.

예시 Reverse‑Proxy 구성

api.example.com {
    reverse_proxy 127.0.0.1:5000
}

Notes

  • 앱 포트는 공개되지 않습니다.
  • 방화벽이 직접 접근을 차단하며, Caddy만 접근할 수 있습니다.
  • NestJS, Next.js (standalone), Express 등에도 적용됩니다.

정적 사이트용 Caddyfile

app.example.com {
    root * /var/apps/frontend/dist
    encode gzip zstd
    try_files {path} {path}/ /index.html
    file_server
}

이것이 하는 일

  • 정적 파일을 직접 제공합니다.
  • 클라이언트‑사이드 라우팅(SPA)을 지원합니다.
  • 압축을 활성화합니다.
  • Node.js 프로세스가 필요하지 않습니다.
  • 80443 포트만 외부에 공개되며, 애플리케이션은 직접 인터넷에 바인딩되지 않습니다.
  • 정적 사이트는 런타임 위험이 전혀 없습니다.
  • 동적 애플리케이션은 systemd와 방화벽 뒤에서 격리됩니다.

이러한 깔끔한 분리는 서버를 보안이 유지되고, 관찰 가능하며, 이해하기 쉬운 상태로 유지합니다.

동적 앱 배포

sudo chown -R dev:dev /var/apps/myapp

cd /var/apps/myapp
git pull
npm ci
npm run build
npm prune --production

sudo chown -R svc-myapp:svc-myapp /var/apps/myapp
sudo chmod -R o-rwx /var/apps/myapp

sudo systemctl restart myapp

⚠️ 절대 sudo git pull 하지 말 것

  • 권한 상승을 방지합니다.
  • 앱 간 횡 이동을 차단합니다.
  • 우발적인 데이터 유출을 방지합니다.
  • 노출된 데이터베이스 위험을 감소시킵니다.
  • 앱 버그로 인한 루트 수준 침해를 방지합니다.

하나의 앱이 해킹당하더라도 시스템은 살아남습니다. 다른 앱도 살아남습니다. 데이터도 살아남습니다.

Security Philosophy

  • Security is not about tools; it’s about clear boundaries and boring defaults.
  • This setup avoids complexity, avoids magic, and relies on Linux doing what it already does best.

If you follow this guide end‑to‑end, your server will already be more secure than most production environments.

Happy (and secure) deploying! 🚀

Back to Blog

관련 글

더 보기 »