
SOFTMOA PROJECTS
대기업 그룹사 전산시스템 보안 고도화 — 외부 취약점진단 전면 대응과 다층 방어 체계 구축
구축 배경과 구현 범위를 확인해보세요.
Contents

대기업 그룹사의 기업 홈페이지와 콘텐츠 관리자 시스템을 새 구조로 재구축하면서, 외부 전문기관의 취약점진단에서 지적된 항목을 코드와 서버 설정으로 모두 조치하고 공격 로그 분석에서 나온 위험까지 막는 다층 방어 체계를 구축했습니다. 진단 보고서를 받아 항목별로 실제 서버에 같은 요청을 재현해 확인하고, 하루 동안 13건의 보안 변경을 테스트 서버에 반영해 응답 헤더 검사까지 배포 자동 검증에 묶었습니다. 점검은 소프트모아가 직접 개발한 보안 점검 프레임워크로 수행했습니다.
프로젝트 개요
기존 시스템은 외부 취약점진단에서 소스코드 영역 개선필요 판정을 받았고, 운영 서버 접근 로그에는 2주 동안 WordPress·PHP 관리도구 탐색 약 4만 8천 건, 환경 파일(.env, .git) 탈취 시도 약 3천 건이 쌓여 있었습니다. 공격 요청이 애플리케이션까지 들어와 서버 오류(5xx)를 낸 기록도 2만 건에 달했습니다.
재구축 시스템에서는 단순히 지적 항목을 하나씩 지우는 데 그치지 않고, 공격이 어디에서 멈춰야 하는지를 층별로 다시 설계했습니다. 클라우드 방화벽, 웹 서버(nginx), 자동 차단(fail2ban), 애플리케이션(Spring Security), 프로세스 송신 통제(systemd), 데이터베이스(PostgreSQL) 각 층이 서로 다른 공격을 맡아, 한 층이 뚫려도 다음 층에서 막히도록 구성했습니다.
| 구분 | 내용 |
|---|---|
| 대상 | 대기업 그룹사 기업 홈페이지(국문·영문, 공개 페이지 106개)와 콘텐츠 관리자 시스템 |
| 범위 | 외부 취약점진단 지적 항목 조치, 공격 로그 분석, 서버·애플리케이션 보안 통제 구축, 코드 보안 점검(OWASP Top 10:2025) |
| 방식 | 진단 요청 재현 → 원인 추적 → 코드·설정 조치 → 회귀 테스트 → 배포 자동 검증 |
| 결과 | 코드·설정으로 고칠 수 있는 지적 항목 전부 조치, 정적 분석 재지적 대비 소명 근거 정리, 실제 악용 가능한 고위험 취약점 0건 |
자체 개발 보안 점검 프레임워크
이번 작업은 소프트모아가 웹 프로젝트용으로 직접 개발해 운영하는 보안 점검 프레임워크로 진행했습니다. 점검을 세 가지 모드로 나누고, 모드마다 수집 스크립트와 판정 기준, 보고서 형식을 정해 두어 누가 점검해도 같은 근거와 같은 결론이 나오게 만든 체계입니다.
| 모드 | 점검 내용 | 근거 자료 |
|---|---|---|
| 버전 취약점(updates) | 설치된 라이브러리·런타임·OS 패키지의 공개 취약점(CVE), 실제 악용 목록(KEV), 악용 가능성 점수(EPSS), 지원 종료(EOL)와 올릴 버전 | 의존성 해석 결과, 서버 패키지 목록 |
| 공격 로그와 보안 통제(posture) | 웹 서버 접근 로그의 공격 징후 분류, 노출·분리·로그인 보호·CSRF·차단·요청 제한·fail2ban·로그 보존·DB 감사·백업·TLS 통제의 충족 여부 | 접근 로그 분석기, 서버 상태 수집기 |
| 코드 취약점(code) | 모든 진입점을 목록화한 뒤 OWASP Top 10:2025 분류별로 인가, 주입, SSRF, 파일 처리, 인증, 예외 처리를 추적 | 진입점 목록, 소스 추적, 기존 테스트 |
- 안전 우선 설계: 모든 모드는 읽기 전용으로 시작합니다. 동작 중인 서버에 공격 요청을 보내지 않고, 점검 중에는 서버·방화벽·차단 상태를 바꾸지 않습니다.
- 정보 보호: 보고서에는 방문자 IP, 쿼리 값, 계정, 세션·토큰 값이 남지 않도록 자동으로 가립니다(출발지는 src-009 같은 익명 번호로 표기).
- 근거 중심 판정: 발견 항목마다 "무엇인지, 어떻게 공격하는지, 판정, 근거, 해결 방법과 담당"을 적고, 판정은 해당·해당(완화)·해당 없음·미확인으로 나눕니다. 확인하지 못한 것은 통과로 쓰지 않습니다.
- 점검 기록 관리: 점검마다 실행 기록과 모드별 보고서를 남기고, 고객 제출 보고서에 인용한 기록은 지워지지 않게 보존합니다.

공격 로그 분석
테스트 서버를 연 지 하루 반 만에 접근 로그 70,849줄을 전수 분석해 자동 스캐너의 탐색을 찾아냈습니다. 서버가 공개되자마자 wpprobe, masscan 같은 스캐너와 브라우저로 위장한 요청이 환경 파일과 관리도구를 찾기 시작했고, 분석 결과를 그대로 차단 규칙으로 옮겼습니다.
| 위험 요소 | 공격 방법 | 막는 장치 |
|---|---|---|
| 환경 파일·저장소 탈취 | /.env, /.git/config, /.ftpconfig 요청 | nginx 서버 단계에서 444로 연결 종료 |
| PHP 관리도구 탐색 | phpMyAdmin, elFinder 커넥터, config.php | PHP·ASP·JSP·CGI 확장자 444 종료 |
| WordPress 탐색 | /wp-login.php, /xmlrpc.php, /wp-json/ | WordPress 경로 444 종료 |
| 경로 조작 | 장비 취약점을 노린 상위 경로(../) 요청 | 경로 정규화, 파일 저장소 루트 이탈 검사 |
| SQL 주입 | OR 1=1 형태의 조건 조작 | jOOQ 값 바인딩 |
| 반복 공격 | 같은 IP의 연속 탐색 | fail2ban이 1시간 동안 80·443 차단 |
수행 업무
1. 공격 경로 차단과 요청 제한
- 숨김 파일(인증서 갱신용 .well-known 제외), PHP·ASP·JSP·CGI 확장자, wp-admin, phpMyAdmin, elFinder 경로는 애플리케이션에 넘기지 않고 nginx 서버 블록에서 444로 바로 끊습니다. 업로드 파일 위치 같은 우선 매칭 규칙보다 먼저 적용되도록 배치했습니다.
- 관리자 로그인은 IP당 분당 10회, 관리자 API는 초당 20회로 제한하고, 넘으면 관리자 화면이 그대로 읽을 수 있는 표준 오류 형식(problem+json) 429를 돌려줍니다.
- 공개 조회수 증가 요청에도 IP당 분당 30회 제한을 걸어 조회수 조작과 쓰기 부하를 막았습니다. 제한에 걸려도 화면은 정상으로 보이고 조회수만 세지 않습니다.
2. fail2ban 자동 차단과 정규식 결함 수정
- 444 차단 응답이 10분에 20회, 요청 제한 초과가 10분에 10회인 IP를 1시간 동안 80·443 포트에서 막습니다.
- fail2ban이 정규식을 대조하기 전에 날짜 부분을 빈 대괄호로 바꾼다는 동작 때문에, 날짜를 요구하던 필터가 차단 응답을 하나도 세지 못하는 결함을 찾아 고쳤습니다. 실제 서버 로그로 444 줄은 모두 적중하고 정상 요청은 한 줄도 걸리지 않는 것을 검증했습니다.
3. 관리자 로그인 무차별 대입 방어
- 같은 계정에 연속 5회, 같은 IP에서 15분 안에 20회 실패하면 15분 동안 비밀번호 확인 없이 429로 거절하고, 거절한 시도도 감사 기록(LOCKED)에 남깁니다.
- 잠금은 회원 식별자 기준이라 대소문자, 악센트, 앞뒤 공백을 바꾼 아이디로 우회할 수 없습니다. 로그인에 성공하면 그 계정의 실패 수를 지웁니다.
- 세션은 로그인 때 ID를 교체하고, 쿠키는 Secure·HttpOnly·SameSite=Lax, 모든 쓰기 요청은 CSRF 토큰을 요구합니다.

4. Content-Security-Policy 도입
- 관리자가 입력한 HTML 본문을 그리는 사이트라, 서버 정화 외에 브라우저 단계의 2중 방어로 CSP를 적용했습니다.
- 공개 사이트는 요청마다 새 nonce를 만들어 script-src 'nonce' 'strict-dynamic'으로 두고, Next.js가 자기 스크립트와 분석·지도·공유 스크립트에 nonce를 붙이도록 구성했습니다. 태그 관리자, 웹 분석, 지도와 공유 SDK가 모두 정상 동작하는 것을 확인했습니다.
- 관리자 앱은 정적 페이지 특성에 맞춘 정책을 따로 두고, 외부 스크립트 출처 제한, object-src 'none', base-uri, frame-ancestors를 적용했습니다.
5. 정보 노출 제거와 보안 헤더 정비
- Server 헤더를 제품명까지 완전히 지우고(nginx headers-more 모듈), Next.js의 X-Powered-By 헤더를 껐습니다.
- 모든 응답에 Referrer-Policy: strict-origin-when-cross-origin, HSTS, nosniff, X-Frame-Options를 정적 파일까지 빠짐없이 붙였습니다.
- 숫자 자리에 문자를 넣으면 400 응답에 프레임워크 형 변환 원문과 내부 Java 형 이름이 나가던 것을 "입력값을 확인해 주세요." 같은 일반 문구로 바꾸고, 필드 이름은 그대로 유지했습니다.
- 외부의 헬스 체크 주소는 404로 숨기고, robots.txt에서 관리자 경로를 지웠습니다. 배포 확인은 내부 주소로 직접 호출하도록 바꿔 운영 영향이 없습니다.
- 외부 주소를 여는 새 창에 noopener, noreferrer를 붙여 열린 페이지가 원래 탭을 피싱 페이지로 바꾸지 못하게 했습니다.
6. TLS 암호군 강화
- TLS 1.3과 1.2만 허용하고, 암호군을 ECDHE + AES-GCM, CHACHA20만 남겨 약한 MAC 지적을 받은 CBC-SHA 계열을 제거했습니다.

7. 오픈 리다이렉트 차단
- 옛 주소를 새 주소로 옮겨 주는 규칙(202개)에서 /pub//외부도메인 같은 입력이 외부 사이트로 이동시킬 수 있는 경로를 찾아, 프레임워크의 경로 정규화에 기대지 않고 평가 함수에서 직접 막았습니다. 데이터 이관 도구의 Kotlin 평가기도 같은 테스트 벡터로 함께 고쳤습니다.
8. SSRF 심층 방어: 애플리케이션 내부망 송신 차단
- 관리자가 입력한 URL을 확인하거나 외부 이미지를 받아오는 기능은 이미 공인 주소만 허용하고 리다이렉트마다 다시 검사합니다. 여기에 DNS 재바인딩으로 검사를 우회하는 경우까지 대비해, 백엔드 프로세스 자체가 사설망, 링크 로컬(클라우드 메타데이터 포함), CGNAT 대역으로는 송수신하지 못하도록 systemd IPAddressDeny로 막았습니다.
- 허용은 localhost와 데이터베이스 주소뿐이며, 데이터베이스 주소는 접속 설정에서 읽어 배포 때 자동으로 반영하고 IPv4가 아니면 배포를 멈춥니다.
9. 데이터베이스 감사와 로그 보존
- PostgreSQL에 접속(인증·인가 단계), 접속 종료, DDL(권한 변경 포함) 기록을 켜고 영구 저널로 보내 1년까지 보존합니다. 실패한 인증은 항상 기록됩니다.
- 웹 서버 로그는 별도 경로에서 하루 단위 120개를 보존해 90일 보존 요건을 넘기고, 배포판 설정 파일을 건드리지 않아 자동 보안 업데이트가 멈추지 않게 했습니다.
- 데이터베이스는 웹 서버와 분리된 별도 서버에서 사설망으로만 열고, 웹 서버 주소만 접속을 허용합니다.
10. 배포 자동 검증과 서비스 가용성
- 배포 스크립트가 배포 직후 공개 화면과 관리자 화면의 CSP, Referrer-Policy, Server 헤더 부재를 직접 검사해, 설정이 빠진 상태로 배포가 끝나지 않게 했습니다. 산출물의 비밀값 포함 여부도 배포마다 검사합니다.
- 모든 웹 서비스가 부팅 때 자동으로 켜지고 비정상 종료되면 5초 안에 다시 뜨도록 구성했습니다.
- 테스트 단계에서는 클라우드 방화벽으로 80·443을 사무실 주소에만 열어 공개 전 노출을 막았습니다.

외부 취약점진단 대응 현황
| 진단 항목 | 조치 |
|---|---|
| 세션 쿠키 Secure 누락 | Secure, HttpOnly, SameSite=Lax 적용 |
| 정적 파일 nosniff, HSTS 누락 | 정적 위치까지 보안 헤더 반복 적용 |
| page, size 오버플로 500 오류 | 범위 검사 후 400 응답, 회귀 테스트 |
| 경로 조작 | 경로 정규화, 루트 이탈 검사, UUID 저장 이름 |
| SSRF | 공인 주소 검사, 리다이렉트 재검사, 프로세스 내부망 송신 차단 |
| 위험한 형식 파일 업로드 | 확장자 허용 목록과 파일 서명 동시 검사, PDF 서명, 이미지 해독 검사 |
| CSP 누락 | 공개 사이트 nonce + strict-dynamic, 관리자 전용 정책 |
| TLS 약한 MAC 암호군 | ECDHE + AES-GCM, CHACHA20만 허용 |
| X-Powered-By, Server 헤더 노출 | 두 헤더 모두 제거 |
| Referrer-Policy 누락 | strict-origin-when-cross-origin |
| 오류 응답의 내부 형 이름 노출 | 일반 안내 문구로 치환 |
| robots.txt 관리자 경로, 헬스 체크 공개 | 관리자 경로 삭제, 외부 헬스 체크 404 |
| 관리자 비밀번호 자동완성 | 자동완성 끔 |
| 로그인 반복 시도 방어 부재 | 계정·IP 기준 15분 잠금, 감사 기록 |
| 접속·권한·DDL 기록 부재 | DB 감사 로그와 1년 보존 |
기술 구성
| 영역 | 구성 |
|---|---|
| Backend | Kotlin 2.4, Spring Boot 4.1, Spring Security(CSRF, 세션, 로그인 잠금), jOOQ, JDK 25 |
| Frontend | Next.js 16.4 App Router(요청별 nonce CSP), React 19.3, TypeScript 6, Tailwind CSS 4 |
| Database | PostgreSQL 18(별도 서버, 사설망, 접속·DDL 감사) |
| Server | Ubuntu, nginx(공격 경로 차단, 요청 제한, TLS 1.3, headers-more), fail2ban, systemd(IPAddressDeny, 자동 재시작), journald |
| 점검 도구 | 소프트모아 보안 점검 프레임워크(버전 취약점, 공격 로그·보안 통제, OWASP Top 10:2025 코드 점검) |
구현 포인트
- 지적 항목을 지우는 것이 아니라 재현부터: 진단과 같은 요청을 실제 서버에 읽기 전용으로 다시 보내 응답을 확인한 뒤 고치고, 고친 뒤 같은 요청으로 다시 확인했습니다.
- 한 층에 기대지 않는 설계: 애플리케이션이 막는 공격도 웹 서버와 운영체제 단계에서 한 번 더 막습니다. SSRF는 코드 검사와 프로세스 송신 차단, XSS는 서버 정화와 CSP, 무차별 대입은 요청 제한과 계정 잠금과 fail2ban이 함께 맡습니다.
- 도구의 실제 동작까지 검증: fail2ban 필터처럼 설정만으로는 동작한다고 볼 수 없는 장치는 실제 로그로 적중과 비적중을 확인했습니다.
- 사용성 하한선 유지: CSP를 넣으면서도 분석, 지도, 공유 기능과 첫 화면 모습이 그대로인지 확인했고, 조회수 제한은 화면에 오류를 보이지 않게 했습니다.
- 되돌아가지 않는 보안: 보안 헤더 검사를 배포 자동 검증에 넣어, 이후 누가 배포하더라도 설정이 빠지면 바로 드러나게 했습니다.
관련 화면
본문의 화면은 고객 시스템의 보안 정보를 보호하기 위해 실제 구성과 점검 결과를 바탕으로 재구성한 예시입니다. 실제 접속 주소, 계정, 서버 이름은 싣지 않았습니다.
