보안 설정
개요
이 문서에서는 PlantPulse 플랫폼의 보안 설정 방법을 안내합니다. 네트워크, 전송, 애플리케이션, 데이터 레벨의 보안 설정을 통해 플랫폼을 안전하게 운영할 수 있습니다.
중요: 산업 제어 시스템(ICS)은 가용성이 최우선이므로, 보안 설정 변경 시 반드시 테스트 환경에서 검증한 후 운영 환경에 적용해 주세요.
보안 아키텍처
PlantPulse의 보안은 4개 계층으로 구성됩니다.
| 계층 | 보안 영역 | 주요 기능 |
|---|---|---|
| 1. 네트워크 | 방화벽, 세그멘테이션, VPN | 외부 접근 차단, 네트워크 분리 |
| 2. 전송 | HTTPS/TLS, 암호화 | 통신 데이터 암호화 |
| 3. 애플리케이션 | 인증, 인가, 필터, 세션 | 사용자 인증 및 권한 관리 |
| 4. 데이터 | DB 접근 제어, 암호화 | 저장 데이터 보호 |
TLS / 인증서 (자동 관리)
인증서를 굽는 것은 plantpulse-certs 원샷 컨테이너 하나입니다. 스택이 뜰 때 가장 먼저 실행되어 prepare-ssl.sh 로 모든 자재를 만들고, pp-security 볼륨에 써 넣은 뒤 스스로 종료합니다. 나머지 컨테이너는 그 볼륨을 읽기 전용으로 마운트해 씁니다.
| 항목 | 값 |
|---|---|
| 굽는 주체 | plantpulse-certs (원샷 — Exited (0) 이 정상) |
| 자재 위치 | 볼륨 pp-security, 컨테이너 안 경로 /var/security/plantpulse |
| 읽는 쪽 | 데이터레이크와 앱 — 모두 :ro |
| 예외 | plantpulse-plugin-opcua-server 만 읽기-쓰기. 기동할 때 보안 temp 디렉토리를 만들어야 하기 때문입니다 |
| 설정 | compose 의 PP_TLS_* 환경변수 |
plantpulse-certs 를 재시작하지 마세요restart: "no" 로 선언된 것은 실수가 아닙니다. 다시 돌면 인증서를 새로 굽습니다. 재발급이 필요할 때만 아래 절차로 명시적으로 수행하세요.
이 컨테이너는 헬스체크가 꺼져 있으므로(healthcheck: disable) 종료 코드로만 판정하세요. 뒤따르는 컨테이너들도 health 가 아니라 «정상 종료됐는가»(service_completed_successfully)를 기다립니다.
env.sh TLS 변수
| 변수 | 기본 | 설명 |
|---|---|---|
PP_TLS_ENABLED | true | TLS 활성화 |
PP_TLS_CERT_DIR | /var/security/plantpulse | 인증서 디렉토리 |
PP_TLS_DOMAIN | plantpulse.io | 기본 도메인 |
PP_TLS_KEYSTORE_PASSWORD | kopens123! (변경 필수) | keystore 비밀번호 |
PP_TLS_TRUSTSTORE_PASSWORD | ${PP_TLS_KEYSTORE_PASSWORD} | truststore 비밀번호 |
PP_TLS_VALID_DAYS | 3650 | 유효 기간 (일) |
PP_TLS_EC_GROUP | secp256r1 | ECDSA 곡선 |
PP_TLS_SIGALG | SHA256withECDSA | 서명 알고리즘 |
PP_TLS_OPCUA_APP_URI | urn:plantpulse:opcua:server | OPC-UA Application URI |
PP_TLS_NODE_NAMES | master worker-1 ... worker-5 | 클러스터 노드명 |
PP_TLS_SAN_DNS | localhost,<hostname>,<domain> | SAN DNS |
PP_TLS_SAN_IPS | <HOST_IP>,<SERVICE_IP>,<PUBLIC_IP>,127.0.0.1 | SAN IP (NAT/외부 IP 포함 필수) |
PP_TLS_FORCE_REGENERATE | false | true 면 강제 재생성 |
인증서 생성 절차
설치할 때 자동으로 한 번 수행되므로, 보통은 아무것도 하지 않습니다. SAN 이 바뀌었거나 만료·손상되었을 때만 아래를 수행합니다.
cd /opt/kopens/plantpulse-platform-docker
# 1. compose 의 PP_TLS_* 검토 (외부 노출 IP/도메인 포함)
vi compose/docker-compose.yml
# 2. 강제 재생성 — 원샷을 다시 돌립니다
PP_TLS_FORCE_REGENERATE=true \
docker compose -f compose/docker-compose.yml up --force-recreate plantpulse-certs
# 3. 자재를 읽는 쪽을 재시작
bin/restart.sh
PP_TLS_SAN_IPS 는 PP_HOST_IP · PP_SERVICE_IP · PP_MASTER_IP · PP_PUBLIC_IP · 127.0.0.1 에서 조립됩니다. 형식이 맞지 않는 값이 하나만 있어도 openssl 이 확장 파일 전체를 거부합니다. DOCKER_PP_EXTERNAL_IP 를 NAT 가 아닌 박스에서 채우지 마세요.
로그 어디에도 인증서 이야기가 나오지 않습니다. compose 는 SAN DNS 에 컨테이너 이름 여덟(localhost · plantpulse-datalake · plantpulse-server-web · plantpulse-batch-web · plantpulse-warehouse · plantpulse-plugin-opcua-server · plantpulse-plugin-aasx-server · plantpulse-proxy)과 개명 전 옛 이름 다섯을 함께 넣습니다. 컨테이너 이름을 바꾸거나 백엔드 호스트를 직접 지정했다면 SAN 목록도 함께 고쳐야 합니다.
prepare-ssl.sh 가 생성하는 산출물:
/var/security/plantpulse/
├── ca.crt / ca.key # Self-signed CA
├── server.jks / server.crt # Tomcat (서버)
├── cassandra.jks # Cassandra internode + client
├── kafka.jks # Kafka broker
├── mqtt.jks # HiveMQ
├── opcua/ # OPC-UA
│ ├── server.pem / server.key
│ └── private/ rejected/ trusted/
└── client/ # 클라이언트 인증서 (선택)
외부 CA 인증서 사용
자체 서명 대신 회사 / Let's Encrypt CA 인증서를 사용하려면:
# 1. CA 발급 인증서를 JKS keystore 로 변환
keytool -importcert -alias rootca -file company-ca.pem \
-keystore /var/security/plantpulse/server.jks \
-storepass ${PP_TLS_KEYSTORE_PASSWORD}
keytool -importkeystore -srckeystore company.p12 -srcstoretype PKCS12 \
-destkeystore /var/security/plantpulse/server.jks \
-deststoretype JKS -deststorepass ${PP_TLS_KEYSTORE_PASSWORD}
# 2. PP_TLS_FORCE_REGENERATE 가 false 인 상태로 재시작
./restart.sh
만료 모니터링: 인증서 만료 30일 전에 알람이 발생하도록 외부 모니터링 (
openssl x509 -enddate) 을 설정해 주세요.
웹 보안 필터
PlantPulse는 web.xml에 정의된 보안 필터를 통해 웹 요청을 보호합니다.
SecurityFilter
인증되지 않은 사용자의 접근을 차단하는 핵심 필터입니다.
<!-- 웹앱 내부 WEB-INF/web.xml (WAR 에 포함 — 배포 시 초기화되므로 영구 변경은 소스에서) -->
<filter>
<filter-name>SecurityFilter</filter-name>
<filter-class>com.kopens.plantpulse.server.web.filter.SecurityFilter</filter-class>
<init-param>
<param-name>excludeUrls</param-name>
<param-value>/api/v5/ping,/login,/css/,/js/,/images/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 모든 요청(
/*)에 대해 인증 여부를 확인합니다. excludeUrls에 지정된 경로는 인증 없이 접근이 가능합니다.- 인증되지 않은 요청은 로그인 페이지로 리다이렉트됩니다.
XSSFilter
Cross-Site Scripting(XSS) 공격을 방지하는 필터입니다.
<filter>
<filter-name>XSSFilter</filter-name>
<filter-class>com.kopens.plantpulse.server.web.filter.XSSFilter</filter-class>
<init-param>
<param-name>excludeUrls</param-name>
<param-value>/api/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>XSSFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- 요청 파라미터에서
<script>,<iframe>등 위험한 태그를 제거합니다. - API 경로는 별도의 입력 검증을 적용하므로 필터에서 제외할 수 있습니다.
CSRF 방어
Cross-Site Request Forgery(CSRF) 공격을 방지하기 위해 토큰 기반 검증을 수행합니다.
<filter>
<filter-name>CSRFFilter</filter-name>
<filter-class>com.kopens.plantpulse.server.web.filter.CSRFFilter</filter-class>
<init-param>
<param-name>excludeUrls</param-name>
<param-value>/api/v5/</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CSRFFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- POST, PUT, DELETE 요청에 대해 CSRF 토큰을 검증합니다.
- 폼 요청 시
_csrf파라미터 또는X-CSRF-TOKEN헤더가 필요합니다. - REST API(
/api/v5/)는 토큰 인증을 사용하므로 CSRF 필터에서 제외됩니다.
EncodingFilter
문자 인코딩을 통일하여 인코딩 관련 보안 취약점을 방지합니다.
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>com.kopens.plantpulse.server.web.filter.EncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
HTTPS 설정
사용자가 보는 80 / 443 은 plantpulse-proxy(nginx)가 종단합니다. 웹 서버 컨테이너는 호스트 포트를 열지 않으므로, 브라우저용 인증서를 바꾸는 곳은 Tomcat 이 아니라 프록시입니다.
프록시 인증서 — 기본은 자체 서명
설치 직후 443 은 아무 준비 없이 바로 열립니다. 프록시가 첫 기동 때 자체 서명 인증서를 만들어 pp-proxy-certs 볼륨에 넣기 때문입니다.
재시작이 고객의 실제 인증서를 자체 서명으로 되돌리는 것은 조용한 사고이므로, 파일이 이미 있으면 프록시는 그대로 씁니다.
실제 인증서로 교체
nginx 설정을 고칠 필요가 없습니다 — 경로가 고정되어 있어 파일만 바꾸면 됩니다.
cd /opt/kopens/plantpulse-platform-docker
# pp-proxy-certs 볼륨의 server.crt / server.key 를 교체
docker cp /path/to/fullchain.crt plantpulse-proxy:/etc/nginx/certs/server.crt
docker cp /path/to/private.key plantpulse-proxy:/etc/nginx/certs/server.key
docker compose -f compose/docker-compose.yml restart plantpulse-proxy
| 항목 | 값 |
|---|---|
| 볼륨 | pp-proxy-certs |
| 컨테이너 안 경로 | /etc/nginx/certs/server.crt · /etc/nginx/certs/server.key |
| 자체 서명 CN | PP_PROXY_CERT_CN (기본 plantpulse.local) |
인증서 파일은 중간 인증서까지 포함한 체인(fullchain) 으로 넣어 주세요.
프록시는 1884 를 passthrough 로 흘려보내고 브로커가 TLS 를 종단합니다. 따라서 프록시 인증서를 바꿔도 MQTT 클라이언트가 검증하는 인증서는 바뀌지 않습니다 — 그쪽은 plantpulse-certs 가 만든 자재를 씁니다.
(참고) 웹 서버에 직접 인증서를 다는 경우
아래는 웹 서버를 프록시 없이 직접 노출하는 환경을 위한 참고입니다. 표준 구성에서는 위의 프록시 인증서만 바꾸면 됩니다. 컨테이너 안 백엔드 구간의 TLS 자재는 plantpulse-certs 원샷이 이미 만들어 둡니다.
HTTPS를 사용하려면 SSL 인증서를 설치해야 합니다.
# 1. 키스토어 생성 (자체 서명 인증서 - 테스트용)
keytool -genkeypair -alias plantpulse -keyalg RSA -keysize 2048 \
-validity 365 -keystore /opt/kopens/plantpulse-platform/plantpulse-server/server/conf/keystore.jks \
-storepass changeit -keypass changeit \
-dname "CN=plantpulse.example.com, OU=IT, O=Company, L=Seoul, ST=Seoul, C=KR"
# 2. 공인 인증서 가져오기 (운영 환경)
keytool -importcert -alias plantpulse -file /path/to/certificate.crt \
-keystore /opt/kopens/plantpulse-platform/plantpulse-server/server/conf/keystore.jks \
-storepass changeit
# 3. 중간 인증서(Chain) 가져오기
keytool -importcert -alias intermediate -file /path/to/intermediate.crt \
-keystore /opt/kopens/plantpulse-platform/plantpulse-server/server/conf/keystore.jks \
-storepass changeit
# 4. 인증서 확인
keytool -list -v -keystore /opt/kopens/plantpulse-platform/plantpulse-server/server/conf/keystore.jks \
-storepass changeit
Tomcat HTTPS 커넥터
Tomcat의 server.xml에 HTTPS 커넥터를 추가합니다.
<!-- plantpulse-server/server/conf/server.xml -->
<!-- HTTP 커넥터 (HTTPS 리다이렉트용) -->
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="443" />
<!-- HTTPS 커넥터 -->
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="200"
SSLEnabled="true"
scheme="https"
secure="true"
keystoreFile="/opt/kopens/plantpulse-platform/plantpulse-server/server/conf/keystore.jks"
keystorePass="changeit"
clientAuth="false"
sslProtocol="TLSv1.2"
sslEnabledProtocols="TLSv1.2,TLSv1.3"
ciphers="TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" />
HTTP에서 HTTPS로 리다이렉트
모든 HTTP 요청을 자동으로 HTTPS로 리다이렉트합니다.
<!-- 웹앱 내부 WEB-INF/web.xml (WAR 에 포함 — 배포 시 초기화되므로 영구 변경은 소스에서) -->
<security-constraint>
<web-resource-collection>
<web-resource-name>Secure</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
TLS 버전 관리
보안을 위해 TLS 1.2 이상만 허용하는 것을 권장합니다.
| TLS 버전 | 상태 | 권장 |
|---|---|---|
| SSLv3 | 사용 금지 | POODLE 취약점 |
| TLS 1.0 | 사용 금지 | 취약점 존재 |
| TLS 1.1 | 사용 금지 | 취약점 존재 |
| TLS 1.2 | 허용 | 권장 |
| TLS 1.3 | 허용 | 최신, 가장 안전 |
API 인증
PlantPulse REST API(/api/v5)는 api_key 를 헤더에 실어 호출합니다. api_key 를 얻는
경로가 두 가지이고, 그 뒤의 호출 방식은 같습니다.
/api/v5/auth 는 새 문자열을 만들어 내지 않습니다. 제출한 토큰이 유효하면 그 토큰 값을
그대로 api_key 로 돌려줍니다(APIManager — api_key = token.getToken()). 토큰은 UUID
형식이며 JWT 가 아닙니다.
토큰 인증 (외부 연동 · 자동화)
사전에 발급받은 API 토큰으로 api_key 를 받습니다. 운영 연동에서 쓰는 경로입니다.
# 1. api_key 발급 — 인증 헤더 없이 호출 가능(V5_Filter bypass 경로)
curl -X POST http://localhost/api/v5/auth \
-H "Content-Type: application/json" \
-d '{"token": "<API_TOKEN>"}'
# 200: {"data": {"api_key": "..."}, "meta": {"status": "OK", ...}, "errors": null}
# 2. 이후 모든 호출에 헤더로 싣는다
curl -X GET http://localhost/api/v5/asset \
-H "X-API-Key: <api_key>"
| 응답 | 뜻 |
|---|---|
200 | 발급 성공 |
400 | 본문이 JSON 이 아니거나 token 이 없음 |
401 (E1100) | 토큰이 유효하지 않음 |
429 (E1101) | brute-force 차단. Retry-After 헤더가 함께 온다 |
brute-force 방어는 IP 단위 카운터 1종입니다. 실패할 때마다 그 IP 의 카운터가 오르고,
윈도우 안에서 한도를 넘으면 429 로 막습니다. plantpulse-engine.properties 에서 조정합니다.
| 프로퍼티 | 기본값 |
|---|---|
engine.api.v5.auth.bruteforce.enabled | true |
engine.api.v5.auth.bruteforce.ip_limit | 30 |
engine.api.v5.auth.bruteforce.window.seconds | 60 — 있으면 window.minutes 보다 우선 |
engine.api.v5.auth.bruteforce.window.minutes | 15 |
X-API-Key 가 1순위이고, 없으면 Authorization: Bearer <api_key> 를 폴백으로 읽습니다.
Bearer 폴백은 그 헤더밖에 못 보내는 MCP 클라이언트를 위한 표준 경로이고, 값은 똑같은
api_key 입니다. Bearer 가 아닌 다른 스킴은 무시합니다.
ID/Password 인증 (콘솔 SPA 전용)
아이디·비밀번호로 로그인해 api_key 를 받습니다. 콘솔 화면이 쓰는 경로이며, 서버 간 연동에는 토큰 인증을 쓰십시오 — 평문 자격증명을 매번 실어 보내는 형태가 되고 brute-force 표면이 넓어집니다.
curl -X POST http://localhost/api/v5/auth/login \
-H "Content-Type: application/json" \
-d '{"userId": "admin", "password": "<PASSWORD>"}'
# 200: {"api_key": "...", "expires_at": 1790000000000, "login_id": "admin"}
# 401: {"error": "..."}
다른 V5 엔드포인트는 {data, meta, errors} 형식이지만 /api/v5/auth/login 은 성공·실패
모두 평문 JSON 입니다. 파서를 공용으로 쓰고 있다면 이 경로만 갈라 주세요.
expires_at 은 epoch 밀리초이고, 영구 토큰이면 null 입니다.
토큰 발급·회수 API (/api/v5/token)
콘솔 System > API 토큰(/token/index) 화면과 같은 일을 REST 로 합니다. 자동 배선
스크립트에서 토큰을 만들고 지울 때 씁니다. 화면 사용법은
보안 관리 를 보세요.
| HTTP | 경로 | 하는 일 |
|---|---|---|
GET | /api/v5/token | 토큰 목록 — token 값은 마스킹(앞 8자 + ****) |
GET | /api/v5/token/{token} | 단일 조회 — 마스킹 |
POST | /api/v5/token | 발급 |
DELETE | /api/v5/token/{token} | 회수(revoke) |
curl -X POST http://localhost/api/v5/token \
-H "X-API-Key: <ADMIN_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"login_id": "svc-mes", "ip": "192.168.0.0/24",
"description": "MES", "ttl_days": 0}'
| 본문 필드 | 설명 |
|---|---|
login_id | 토큰을 받을 계정. 생략하면 호출자(X-API-Key 소유자) 로 발급된다 |
ip | 토큰 사용을 허용할 IP 패턴 |
description | 용도 메모 |
ttl_days | 유효기간(일). 생략하면 전역 기본값(engine.api.token.default_ttl_days), 0 이면 영구, 양수면 그 일수 |
요청 계정(X-API-Key 소유자)의 역할이 ADMIN 이 아니면 403 E1200 이고 감사 로그에
TOKEN_ISSUE_DENIED 가 1건 남습니다. API 역할 계정이 스스로 또는 남의 토큰을 찍어내는
권한상승을 막기 위한 것입니다. 대상 계정(login_id)의 역할은 무관합니다.
ttl_days 는 이 자격을 넓히지 않습니다.
ttl_days 가 왜 필요했나전역 기본값을 0(영구)으로 바꾸면 고객이 화면에서 만든 토큰까지 전부 영구가 됩니다.
자동 배선용 토큰 하나만 영구로 두려고 전역을 건드리는 일을 없애려는 자리입니다.
음수나 정수가 아닌 값은 400 VALIDATION_FAILED 입니다 — 이상한 값을 삼켜서 조용히
영구 토큰이 되는 쪽이 더 위험하기 때문입니다.
발급 성공 응답에는 token 원문이 1회 포함됩니다. 목록·조회에는 다시 나오지 않으므로
이때 받아서 보관하십시오. 발급은 감사에 TOKEN_ISSUED 로 남고, 영구 토큰은 만료 자리에
PERMANENT 로 기록됩니다.
비밀번호 보안
비밀번호 해싱
PlantPulse는 사용자 비밀번호를 SHA-256 해시로 저장합니다. 원본 비밀번호는 서버에 저장되지 않습니다.
비밀번호 정책 권장값
| 항목 | 권장값 | 설명 |
|---|---|---|
| 최소 길이 | 8자 이상 | 12자 이상 권장 |
| 복잡도 | 3종 이상 조합 | 영문 대/소문자, 숫자, 특수문자 중 3종 |
| 변경 주기 | 90일 | 분기별 변경 |
| 이력 관리 | 최근 3개 | 이전 비밀번호 재사용 금지 |
| 연속 실패 잠금 | 5회 | 5회 연속 실패 시 계정 잠금 |
| 잠금 해제 | 30분 또는 관리자 | 자동 해제 또는 수동 해제 |
HTTP 보안 헤더
웹 브라우저의 보안 기능을 활성화하기 위해 HTTP 응답 헤더를 설정합니다.
권장 보안 헤더
| 헤더 | 값 | 설명 |
|---|---|---|
X-XSS-Protection | 1; mode=block | 브라우저 XSS 필터 활성화 |
X-Content-Type-Options | nosniff | MIME 타입 스니핑 방지 |
X-Frame-Options | SAMEORIGIN | 클릭재킹(Clickjacking) 방지 |
Content-Security-Policy | default-src 'self' | 콘텐츠 소스 제한 |
Strict-Transport-Security | max-age=31536000; includeSubDomains | HTTPS 강제 (HSTS) |
Referrer-Policy | strict-origin-when-cross-origin | 리퍼러 정보 제한 |
Nginx 설정 예시
리버스 프록시로 Nginx를 사용하는 경우 다음과 같이 보안 헤더를 추가합니다.
server {
listen 443 ssl http2;
server_name plantpulse.example.com;
# SSL 인증서
ssl_certificate /etc/ssl/certs/plantpulse.crt;
ssl_certificate_key /etc/ssl/private/plantpulse.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 보안 헤더
add_header X-XSS-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
location / {
proxy_pass http://127.0.0.1:80;
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;
}
}
# HTTP → HTTPS 리다이렉트
server {
listen 80;
server_name plantpulse.example.com;
return 301 https://$host$request_uri;
}
HTTP 메소드 제한
불필요한 HTTP 메소드를 차단하여 정보 노출을 방지합니다.
<!-- 웹앱 내부 WEB-INF/web.xml (WAR 에 포함 — 배포 시 초기화되므로 영구 변경은 소스에서) -->
<security-constraint>
<web-resource-collection>
<web-resource-name>RestrictMethods</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
<http-method>HEAD</http-method>
</web-resource-collection>
<auth-constraint/>
</security-constraint>
안내: OPTIONS 메소드는 CORS 프리플라이트 요청에 사용됩니다. 외부 시스템과 API 연동이 필요한 경우 OPTIONS 메소드를 허용해야 할 수 있습니다.
세션 보안
쿠키 보안 설정
<!-- 웹앱 내부 WEB-INF/web.xml (WAR 에 포함 — 배포 시 초기화되므로 영구 변경은 소스에서) -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only> <!-- JavaScript에서 쿠키 접근 차단 -->
<secure>true</secure> <!-- HTTPS에서만 쿠키 전송 -->
<max-age>1800</max-age> <!-- 쿠키 유효 시간 (초) -->
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
| 설정 | 값 | 설명 |
|---|---|---|
http-only | true | XSS를 통한 세션 탈취 방지 |
secure | true | HTTP에서 쿠키가 전송되지 않도록 방지 (HTTPS 필수) |
tracking-mode | COOKIE | URL에 세션 ID가 노출되지 않도록 방지 |
Session Fixation 방지
PlantPulse는 로그인 성공 시 세션 ID를 재생성하여 Session Fixation 공격을 방지합니다. 이 기능은 SecurityFilter에 내장되어 있으며, 별도 설정 없이 자동으로 적용됩니다.
데이터베이스 보안
PostgreSQL
pg_hba.conf 파일에서 접근 허용 범위를 설정합니다.
# /opt/kopens/plantpulse-platform/plantpulse-storage/db/postgres/data/pg_hba.conf
# TYPE DATABASE USER ADDRESS METHOD
# 로컬 접근 (Unix 소켓)
local all all md5
# 로컬호스트 접근
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
# PlantPulse 서버 접근 (특정 IP만 허용)
host plantpulse plantpulse 10.0.0.0/24 md5
# 그 외 접근 차단 (기본)
# host all all 0.0.0.0/0 reject
추가 보안 설정 (postgresql.conf):
# 접속 제한
listen_addresses = '127.0.0.1' # 로컬만 허용 (기본값: '*')
max_connections = 200
# 인증 타임아웃
authentication_timeout = 60 # 초
# SSL 활성화
ssl = on
ssl_cert_file = '/path/to/server.crt'
ssl_key_file = '/path/to/server.key'
# 로그
log_connections = on
log_disconnections = on
log_statement = 'ddl' # DDL 문만 로깅
Cassandra
# /opt/kopens/plantpulse-platform/plantpulse-storage/db/cassandra/conf/cassandra.yaml
# 인증 활성화
authenticator: PasswordAuthenticator
# 인가 활성화
authorizer: CassandraAuthorizer
# 클라이언트 암호화 (TLS)
client_encryption_options:
enabled: true
optional: false
keystore: /opt/kopens/plantpulse-platform/plantpulse-storage/db/cassandra/conf/.keystore
keystore_password: changeit
truststore: /opt/kopens/plantpulse-platform/plantpulse-storage/db/cassandra/conf/.truststore
truststore_password: changeit
protocol: TLS
algorithm: SunX509
cipher_suites: [TLS_RSA_WITH_AES_256_CBC_SHA]
# 노드 간 암호화
server_encryption_options:
internode_encryption: all
keystore: /opt/kopens/plantpulse-platform/plantpulse-storage/db/cassandra/conf/.keystore
keystore_password: changeit
truststore: /opt/kopens/plantpulse-platform/plantpulse-storage/db/cassandra/conf/.truststore
truststore_password: changeit
Redis (Valkey)
# /opt/kopens/plantpulse-platform/plantpulse-storage/db/valkey/conf/valkey.conf
# 비밀번호 설정
requirepass your_strong_password_here
# 바인드 주소 (로컬만 허용)
bind 127.0.0.1
# 보호 모드 활성화
protected-mode yes
# 위험 명령어 비활성화
rename-command FLUSHALL ""
rename-command FLUSHDB ""
rename-command CONFIG ""
rename-command DEBUG ""
rename-command SHUTDOWN PLANTPULSE_SHUTDOWN
네트워크 보안
방화벽 설정
PlantPulse에서 외부 접근이 필요한 포트만 방화벽에서 허용해 주세요.
# firewalld 설정 예시
# 웹 서비스 (외부 접근 허용)
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
# 내부 서비스 (외부 접근 차단 - 기본값)
# 아래 포트들은 외부에서 접근하지 않도록 해 주세요
# PostgreSQL: 5432, Redis: 6379, Cassandra: 9042
# Kafka: 9092, MQTT: 1883
# OPC Agent (필요한 클라이언트 IP만 허용)
sudo firewall-cmd --permanent --add-rich-rule='
rule family="ipv4"
source address="10.0.1.0/24"
port protocol="tcp" port="60000"
accept'
# 적용
sudo firewall-cmd --reload
# 확인
sudo firewall-cmd --list-all
네트워크 세그멘테이션
산업 환경에서는 네트워크를 다음과 같이 분리하는 것을 권장합니다.
| 네트워크 영역 | 용도 | 예시 |
|---|---|---|
| OT 네트워크 | PLC, SCADA, 센서 | 10.0.1.0/24 |
| DMZ | PlantPulse 웹서버, API | 10.0.2.0/24 |
| IT 네트워크 | 사용자 접근, 관리 | 10.0.3.0/24 |
| 데이터 네트워크 | DB, 메시징 | 10.0.4.0/24 |
- OT 네트워크와 IT 네트워크는 직접 통신하지 않도록 해 주세요.
- PlantPulse 서버는 DMZ에 배치하여 양쪽 네트워크와 제한된 통신만 허용합니다.
- 데이터베이스는 데이터 네트워크에 배치하고, PlantPulse 서버에서만 접근을 허용합니다.
VPN 접근
원격에서 관리 콘솔에 접근해야 하는 경우 VPN을 통해 접속하도록 구성합니다.
- 관리 콘솔 및 SSH 접근은 반드시 VPN을 통해서만 허용해 주세요.
- VPN 계정은 개인별로 발급하고, 공유 계정을 사용하지 않도록 합니다.
- VPN 접속 로그를 기록하고 주기적으로 검토합니다.
보안 점검 체크리스트
일일 점검
| 점검 항목 | 확인 방법 |
|---|---|
| 비정상 로그인 시도 | 감사 로그에서 로그인 실패 횟수 확인 |
| 시스템 에러 로그 | 보안 관련 에러/경고 로그 확인 |
| 서비스 가용성 | 헬스체크 API 응답 확인 |
주간 점검
| 점검 항목 | 확인 방법 |
|---|---|
| 사용자 계정 현황 | 미사용 계정, 비활성 계정 점검 |
| 권한 변경 이력 | 감사 로그에서 권한 변경 확인 |
| 방화벽 로그 | 차단된 접근 시도 확인 |
| SSL 인증서 유효기간 | 인증서 만료일 확인 |
월간 점검
| 점검 항목 | 확인 방법 |
|---|---|
| OS 보안 패치 | 최신 보안 업데이트 적용 확인 |
| 비밀번호 정책 준수 | 장기 미변경 비밀번호 확인 |
| DB 접근 권한 | pg_hba.conf 등 접근 제어 설정 점검 |
| 백업 데이터 보안 | 백업 파일 암호화 및 접근 권한 확인 |
| 포트 스캔 | 불필요하게 열린 포트 확인 |
| TLS 설정 | 취약한 프로토콜/암호화 스위트 점검 |
보안 사고 대응
보안 사고가 발생했거나 의심되는 경우 다음 절차에 따라 대응해 주세요.
1단계: 탐지 및 격리
# 의심 IP 차단
sudo firewall-cmd --add-rich-rule='rule family="ipv4" source address="의심IP" drop' --permanent
sudo firewall-cmd --reload
# 의심 사용자 계정 비활성화 (관리 콘솔 또는 DB)
# 관리 콘솔: 보안 관리 > 사용자 관리 > 상태 변경
# 현재 활성 세션 확인
curl -X GET http://localhost/api/v5/sessions \
-H "Authorization: Bearer <admin-token>"
2단계: 분석
# 로그인 이력 조회
grep "LOGIN\|LOGOUT\|AUTH" /opt/kopens/plantpulse-platform/plantpulse-server/logs/system.log
# 접근 로그 분석
grep "의심IP" /opt/kopens/plantpulse-platform/plantpulse-server/server/logs/localhost_access_log.*.txt
# DB 접근 로그 (PostgreSQL)
grep "의심IP" /opt/kopens/plantpulse-platform/plantpulse-storage/db/postgres/data/log/postgresql-*.log
3단계: 복구
- 침해된 계정의 비밀번호를 변경합니다.
- 영향 받은 시스템의 무결성을 확인합니다.
- 필요 시 백업에서 데이터를 복원합니다.
4단계: 사후 조치
- 사고 보고서를 작성합니다.
- 재발 방지 대책을 수립합니다.
- 보안 설정을 강화합니다.
- 관련 인원에게 보안 교육을 실시합니다.
연락처: 보안 관련 문의 사항은 webmaster@kopens.com 으로 연락해 주세요.