Express.js 백엔드 보안 (Nginx + VPS와 함께)

발행: (2026년 1월 12일 오후 02:47 GMT+9)
10 분 소요
원문: Dev.to

I’m happy to translate the article for you, but I’ll need the full text you’d like translated. Could you please paste the content (excluding the source line you’ve already provided) here? Once I have the article text, I’ll translate it into Korean while preserving the original formatting, markdown, and code blocks.

Table of Contents

  1. 위협 모델
  2. 백엔드 보안이 중요한 이유
  3. 보안 기준
  4. 프로젝트 설정
  5. 보안 헤더
  6. CORS 전략
  7. 인증 및 권한 부여
  8. 입력 검증 및 정화
  9. 속도 제한
  10. 요청 크기 제한
  11. 로깅 및 감사
  12. 비밀 및 환경 보안
  13. TLS (HTTPS) 및 Nginx 구성
  14. Nginx 속도 제한
  15. 기본 봇/스캐너 감소
  16. Nginx에서 권장하는 보안 헤더
  17. SSH 강화
  18. 방화벽 (UFW)
  19. Fail2ban
  20. 자동 보안 업데이트
  21. 배포 체크리스트

위협 모델

퍼블릭 백엔드의 일반적인 공격 벡터

  • 무차별 대입 로그인 시도
  • API 남용 / 스크래핑
  • 자격 증명 스터핑
  • 인젝션 공격 (SQL/NoSQL)
  • 잘못 구성된 CORS
  • 토큰 도난 (JWT, 세션 쿠키)
  • 취약점 스캐너
  • 리버스 프록시 우회 (직접 포트 접근)
  • 약한 SSH로 인한 서버 침해

영향 매트릭스

위협영향
NoSQL 인젝션데이터베이스 손상
자격 증명 스터핑계정 탈취
JWT 도난전체 사용자 가장
CSRF무단 행동
XSS토큰 및 세션 유출
API 남용서버 및 결제 악용

목표는 여러 독립적인 레이어를 사용하여 위험을 감소시키는 것.

보안 기준

제어설명
HTTPS 적용모든 트래픽 암호화
강력한 인증 전략JWT 또는 세션 기반
엄격한 CORS허용된 출처만
서버 측 검증클라이언트 데이터를 절대 신뢰하지 않음
속도 제한무차별 공격 및 악용 방지
중앙 집중식 로깅감사 로그
Nginx 리버스 프록시 보호내부 포트 숨김
방화벽 + SSH 강화공격 표면 최소화
Fail2ban + 자동 보안 패치사후 및 사전 방어

프로젝트 설정

# Core dependencies
npm i express helmet cors express-rate-limit cookie-parser compression

# Validation, logging, env handling
npm i zod pino pino-http dotenv

보안 헤더

Helmet은 합리적인 HTTP 보안 헤더 세트를 적용합니다.

import helmet from "helmet";

app.use(helmet());

Note: 순수 API의 경우 HTML 페이지도 제공하지 않는다면 공격적인 CSP를 생략할 수 있습니다.

CORS 전략

프로덕션 환경에서는 *를 절대 사용하지 마세요. 특히 쿠키나 인증 정보가 포함된 경우에는 더욱 그렇습니다.

import cors from "cors";

const allowedOrigins = [
  "https://your-frontend.com",
  "https://www.your-frontend.com",
];

app.use(
  cors({
    origin: (origin, cb) => {
      // Allow non‑browser requests (e.g., Postman) or whitelisted origins
      if (!origin) return cb(null, true);
      if (allowedOrigins.includes(origin)) return cb(null, true);
      return cb(new Error("CORS blocked"), false);
    },
    credentials: true,
    methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
  })
);

Source:

인증 및 인가

ConceptDescription
Authentication클라이언트의 신원을 증명함 (예: 로그인, JWT 발급).
Authorization인증된 신원이 할 수 있는 일을 결정함 (역할 기반 검사).

모범 사례

  • 짧은 수명의 액세스 토큰 사용, 리프레시 토큰 회전.
  • 특권 라우트에 대한 역할 기반 검사.
// Example role guard
export function requireRole(...roles) {
  return (req, res, next) => {
    if (!req.user || !roles.includes(req.user.role)) {
      return res.status(403).json({ status: false, message: "Forbidden" });
    }
    next();
  };
}

입력 검증 및 정화

임시 검증보다 스키마 검증(예: Zod)을 선호합니다.

import { z } from "zod";

const createUserSchema = z.object({
  name: z.string().min(2).max(80),
  email: z.string().email(),
  password: z.string().min(8).max(72),
});

export function validate(schema) {
  return (req, res, next) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        status: false,
        message: "Validation failed",
        errors: result.error.issues,
      });
    }
    req.body = result.data; // use sanitized data downstream
    next();
  };
}

사용법

app.post("/api/users", validate(createUserSchema), (req, res) => {
  // req.body is guaranteed to match the schema
});

속도 제한

엔드포인트 카테고리별로 다른 제한을 적용합니다.

import rateLimit from "express-rate-limit";

export const globalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 600,                // 600 requests per window per IP
  standardHeaders: true,
  legacyHeaders: false,
});

export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 20, // stricter for login/registration
  message: { message: "Too many login attempts. Try again later." },
});

app.use(globalLimiter);               // applies to all routes
app.use("/api/auth", authLimiter);    // only auth routes

요청 크기 제한

페이로드 남용 방지:

app.use(express.json({ limit: "200kb" }));
app.use(express.urlencoded({ extended: true, limit: "200kb" }));

로깅 및 감사

구조화된 로깅은 분석을 더 쉽게 합니다.

import pino from "pino";
import pinoHttp from "pino-http";

const logger = pino({ level: process.env.LOG_LEVEL || "info" });
app.use(pinoHttp({ logger }));

로그 (하지만 비밀번호, 토큰, 원시 비밀은 절대 로그에 남기지 말 것):

  • 인증 실패
  • 의심스러운 트래픽 급증
  • 속도 제한 차단

Secrets & Environment Security

  • 개발에서는 비밀을 .env에 저장하고 프로덕션에서는 서버의 환경 변수에 저장합니다.
  • 절대 .env를 버전 관리에 커밋하지 마세요.
  • 손상된 키를 즉시 교체하세요.
# .env (example)
NODE_ENV=production
PORT=3000
JWT_SECRET=YOUR-JWT-SECRET-KEY

TLS (HTTPS) 및 Nginx 리버스 프록시

아키텍처

Internet → Nginx (443) → Express (127.0.0.1:8000)

Certbot 설치 및 인증서 발급

sudo apt update
sudo apt install nginx certbot python3-certbot-nginx -y

# Issue a certificate for your API sub‑domain
sudo certbot --nginx -d api.yourdomain.com

# Verify auto‑renewal timer
sudo systemctl status certbot.timer

Nginx Rate Limiting

Add the following to /etc/nginx/nginx.conf (inside the http {} block):

limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

Example Server Block

server {
    listen 443 ssl http2;
    server_name api.yourdomain.com;

    # TLS (handled by Certbot)
    ssl_certificate /etc/letsencrypt/live/api.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.yourdomain.com/privkey.pem;

    # Rate limiting
    limit_req zone=api_limit burst=20 nodelay;
    limit_conn conn_limit 20;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Timeouts
        proxy_connect_timeout 10s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;
    }
}

기본 Bot/Scanner 차단

Nginx 수준에서 일반적인 불필요한 요청을 차단합니다.

# Block WordPress xmlrpc pingbacks (if you don't run WP)
location = /xmlrpc {
    deny all;
}

필요에 따라 더 많은 패턴을 추가하세요 (예: /.env, /admin, 알려진 스캐너 사용자‑agents).

Nginx에서 권장하는 보안 헤더

add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header Permissions-Policy "geolocation=(), microphone=()" always;
# (CSP can be added if you serve HTML)

SSH 하드닝

# Disable root login
sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config

# Use key‑based auth only
sudo sed -i 's/PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config

# Change default port (optional)
# sudo sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config

sudo systemctl restart sshd
  • 개인 키를 안전하게 보관하십시오.
  • 패스프레이즈와 SSH 에이전트를 사용하십시오.

방화벽 (UFW)

sudo ufw default deny incoming
sudo ufw default allow outgoing

# Allow SSH (adjust port if you changed it)
sudo ufw allow 22/tcp   # or 2222/tcp if you changed the port

# Allow HTTP/HTTPS (handled by Nginx)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp

sudo ufw enable
sudo ufw status verbose

Fail2ban

sudo apt install fail2ban -y

# Basic jail for SSH
cat <<EOF > /etc/fail2ban/jail.local
[sshd]
enabled = true
port    = ssh
logpath = %(sshd_log)s
maxretry = 5
EOF

프론트엔드를 절대 신뢰하지 마세요. 백엔드만 신뢰하고 모든 것을 검증하세요.

자동 보안 업데이트

sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure --priority=low unattended-upgrades

Deployment Checklist

  • Nginx + Certbot을 통한 HTTPS 적용
  • Helmet 보안 헤더 적용
  • 엄격한 CORS 화이트리스트
  • Zod(또는 유사 도구)를 사용한 입력 검증
  • 속도 제한(Express 및 Nginx)
  • 요청 크기 제한 설정
  • 구조화된 로깅(pino)
  • 비밀 정보를 안전하게 저장하고, 버전 관리 시스템에 포함되지 않음
  • SSH 키 전용 인증, root 로그인 비활성화
  • UFW 방화벽 규칙 적용
  • SSH용 Fail2ban 활성화(선택적으로 Nginx도)
  • 자동 보안 업데이트 활성화

최종 생각

Express.js 백엔드 보안은 단일 설정이나 모듈이 아니라 하나의 분야입니다. 강력한 애플리케이션‑레벨 제한, 강화된 리버스 프록시, 그리고 적절히 잠긴 서버 환경이 실제 보안의 구성 요소입니다.

Express, Nginx, 그리고 VPS‑레벨 하드닝을 활용해 계층형 방어를 구현하면 API의 공격 표면을 크게 줄이고 사용자, 데이터, 비즈니스 로직을 실제 위협으로부터 보호할 수 있습니다.

장기적인 프로덕션 사용을 위해서는 보안된 백엔드가 더 안전할 뿐만 아니라 더 신뢰할 수 있고, 확장 가능하며, 신뢰성을 갖습니다.

Back to Blog

관련 글

더 보기 »

안녕, 뉴비 여기요.

안녕! 나는 다시 S.T.E.M. 분야로 돌아가고 있어. 에너지 시스템, 과학, 기술, 공학, 그리고 수학을 배우는 것을 즐겨. 내가 진행하고 있는 프로젝트 중 하나는...