본문으로 건너뛰기

보안 설정

개요

이 문서에서는 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_ENABLEDtrueTLS 활성화
PP_TLS_CERT_DIR/var/security/plantpulse인증서 디렉토리
PP_TLS_DOMAINplantpulse.io기본 도메인
PP_TLS_KEYSTORE_PASSWORDkopens123! (변경 필수)keystore 비밀번호
PP_TLS_TRUSTSTORE_PASSWORD${PP_TLS_KEYSTORE_PASSWORD}truststore 비밀번호
PP_TLS_VALID_DAYS3650유효 기간 (일)
PP_TLS_EC_GROUPsecp256r1ECDSA 곡선
PP_TLS_SIGALGSHA256withECDSA서명 알고리즘
PP_TLS_OPCUA_APP_URIurn:plantpulse:opcua:serverOPC-UA Application URI
PP_TLS_NODE_NAMESmaster worker-1 ... worker-5클러스터 노드명
PP_TLS_SAN_DNSlocalhost,<hostname>,<domain>SAN DNS
PP_TLS_SAN_IPS<HOST_IP>,<SERVICE_IP>,<PUBLIC_IP>,127.0.0.1SAN IP (NAT/외부 IP 포함 필수)
PP_TLS_FORCE_REGENERATEfalsetrue 면 강제 재생성

인증서 생성 절차

설치할 때 자동으로 한 번 수행되므로, 보통은 아무것도 하지 않습니다. 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
SAN 에 유효하지 않은 IP 가 하나라도 섞이면 인증서가 «한 장도» 생기지 않습니다

PP_TLS_SAN_IPSPP_HOST_IP · PP_SERVICE_IP · PP_MASTER_IP · PP_PUBLIC_IP · 127.0.0.1 에서 조립됩니다. 형식이 맞지 않는 값이 하나만 있어도 openssl 이 확장 파일 전체를 거부합니다. DOCKER_PP_EXTERNAL_IP 를 NAT 가 아닌 박스에서 채우지 마세요.

앱이 백엔드에 붙을 때 쓰는 이름이 SAN 에 없으면 «TimeoutException» 으로만 보입니다

로그 어디에도 인증서 이야기가 나오지 않습니다. 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
자체 서명 CNPP_PROXY_CERT_CN (기본 plantpulse.local)

인증서 파일은 중간 인증서까지 포함한 체인(fullchain) 으로 넣어 주세요.

MQTT TLS(1884)는 프록시가 종단하지 않습니다

프록시는 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_key 는 곧 토큰 값입니다

/api/v5/auth 는 새 문자열을 만들어 내지 않습니다. 제출한 토큰이 유효하면 그 토큰 값을 그대로 api_key 로 돌려줍니다(APIManagerapi_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.enabledtrue
engine.api.v5.auth.bruteforce.ip_limit30
engine.api.v5.auth.bruteforce.window.seconds60 — 있으면 window.minutes 보다 우선
engine.api.v5.auth.bruteforce.window.minutes15
헤더 두 가지

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": "..."}
이 응답만 envelope 이 아닙니다

다른 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 이면 영구, 양수면 그 일수
발급은 ADMIN 만 가능합니다

요청 계정(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-Protection1; mode=block브라우저 XSS 필터 활성화
X-Content-Type-OptionsnosniffMIME 타입 스니핑 방지
X-Frame-OptionsSAMEORIGIN클릭재킹(Clickjacking) 방지
Content-Security-Policydefault-src 'self'콘텐츠 소스 제한
Strict-Transport-Securitymax-age=31536000; includeSubDomainsHTTPS 강제 (HSTS)
Referrer-Policystrict-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-onlytrueXSS를 통한 세션 탈취 방지
securetrueHTTP에서 쿠키가 전송되지 않도록 방지 (HTTPS 필수)
tracking-modeCOOKIEURL에 세션 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
DMZPlantPulse 웹서버, API10.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 으로 연락해 주세요.