Express.js 백엔드 보안 (Nginx + VPS와 함께)
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
- 위협 모델
- 백엔드 보안이 중요한 이유
- 보안 기준
- 프로젝트 설정
- 보안 헤더
- CORS 전략
- 인증 및 권한 부여
- 입력 검증 및 정화
- 속도 제한
- 요청 크기 제한
- 로깅 및 감사
- 비밀 및 환경 보안
- TLS (HTTPS) 및 Nginx 구성
- Nginx 속도 제한
- 기본 봇/스캐너 감소
- Nginx에서 권장하는 보안 헤더
- SSH 강화
- 방화벽 (UFW)
- Fail2ban
- 자동 보안 업데이트
- 배포 체크리스트
위협 모델
퍼블릭 백엔드의 일반적인 공격 벡터
- 무차별 대입 로그인 시도
- 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: …
인증 및 인가
| Concept | Description |
|---|---|
| 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의 공격 표면을 크게 줄이고 사용자, 데이터, 비즈니스 로직을 실제 위협으로부터 보호할 수 있습니다.
장기적인 프로덕션 사용을 위해서는 보안된 백엔드가 더 안전할 뿐만 아니라 더 신뢰할 수 있고, 확장 가능하며, 신뢰성을 갖습니다.