본문으로 건너뛰기

보안 설정

개요​

이 문서에서는 PlantPulse 플랫폼의 보안 설정 방법을 안내합니다. 네트워크, 전송, 애플리케이션, 데이터 레벨의 보안 설정을 통해 플랫폼을 안전하게 운영할 수 있습니다.

중요: 산업 제어 시스템(ICS)은 가용성이 최우선이므로, 보안 설정 변경 시 반드시 테스트 환경에서 검증한 후 운영 환경에 적용해 주세요.

보안 아키텍처​

PlantPulse의 보안은 4개 계층으로 구성됩니다.

계층보안 영역주요 기능
1. 네트워크방화벽, 세그멘테이션, VPN외부 접근 차단, 네트워크 분리
2. 전송HTTPS/TLS, 암호화통신 데이터 암호화
3. 애플리케이션인증, 인가, 필터, 세션사용자 인증 및 권한 관리
4. 데이터DB 접근 제어, 암호화저장 데이터 보호

TLS / 인증서 (자동 관리)​

인증서를 굽는 것은 plantpulse-certs 컨테이너 하나입니다. 스택이 뜰 때 가장 먼저 실행되어 pd_ssl_prepare(plantpulse-datalake-cli 의 lib/lib-ssl.sh)로 모든 자재를 만들고 pp-security 볼륨에 써 넣습니다. 그다음에는 떠 있으면서 6시간마다 만료를 확인하고, 만료 30일 안으로 들어왔을 때만 다시 발급합니다. 나머지 컨테이너는 그 볼륨을 읽기 전용으로 마운트해 씁니다.

항목값
굽는 주체plantpulse-certs (상시 동작 — (healthy) 가 정상)
자재 위치볼륨 pp-security, 컨테이너 안 경로 /var/security/plantpulse
읽는 쪽데이터레이크와 앱 — 모두 :ro
예외plantpulse-plugin-opcua 만 읽기-쓰기. 기동할 때 보안 temp 디렉토리를 만들어야 하기 때문입니다
설정compose 의 PP_TLS_* 환경변수
재시작해도 인증서를 새로 굽지 않습니다

pd_ssl_prepare 는 요청받은 SAN 이 .certs_generated 에 기록된 것과 다를 때만 다시 발급합니다. 그래서 컨테이너를 재시작해도 자재가 바뀌지 않습니다. 재발급이 필요하면 아래 절차로 명시적으로 수행하세요.

restart: unless-stopped 입니다 — 사람이 일부러 멈춘 경우에는 Docker 데몬을 재시작해도 멈춘 채로 둡니다. 사고 대응 중에 손으로 자재를 넣어 둔 상태가 보존됩니다.

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

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

로그 어디에도 인증서 이야기가 나오지 않습니다. PP_TLS_SAN_DNS 의 기본값은 스물한 개이고 세 묶음입니다.

묶음개수이름
지금 쓰는 컨테이너 이름8localhost · plantpulse-datalake · plantpulse-server-web · plantpulse-batch-web · plantpulse-warehouse-web · plantpulse-plugin-opcua · plantpulse-plugin-aasx · plantpulse-proxy
개명 전 옛 이름8plantpulse-warehouse · plantpulse-server · plantpulse-batch · plantpulse-warehouse-s3 · plantpulse-opcua · plantpulse-aasx · plantpulse-plugin-opcua-server · plantpulse-plugin-aasx-server
워커 노드5plantpulse-datalake-worker-1 ~ -5
워커 이름은 2026-09-09 에 들어갔습니다

그전까지 발급된 인증서에는 워커가 한 개도 없었고, 워커 IP 도 IP SAN 에 없었습니다 (dev 41 실측: DNS 이름 열여섯 중 워커 0개). 그래서 워커에서 TLS 를 끊는 것은 클라이언트가 검증할 이름도 주소도 갖지 못했습니다. 그 이전에 구운 인증서를 쓰고 있는 클러스터라면 재발급이 필요합니다.

컨테이너 이름을 바꾸거나 백엔드 호스트를 직접 지정했다면 SAN 목록도 함께 고쳐야 합니다. 워커를 여섯 대 이상으로 늘렸다면 PP_TLS_SAN_DNS 에 그 이름도 추가해야 합니다 — 기본값은 다섯까지입니다. :::

pd_ssl_prepare 가 생성하는 산출물:

/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 로 돌려줍니다(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.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) — 수행자는 X-API-Key 소유자
POST/api/v5/admin/tokens/{token}/revokeADMIN 전용 회수 — 남의 토큰을 끊을 때
회수 경로가 둘인 이유

DELETE /api/v5/token/{token} 은 자기 토큰을 정리하는 경로이고, POST /api/v5/admin/tokens/{token}/revoke 는 관리자가 남의 토큰을 끊는 경로입니다. 후자는 ADMIN role 만 통과합니다 — API role 토큰으로는 호출해도 403 입니다. 사고 대응에서 쓰는 것은 후자입니다.

둘 다 revoked_at 을 찍고 토큰 캐시를 무효화하므로 즉시 막힙니다.

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 문만 로깅

Aster TSDB​

# /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)
# 관리 콘솔: 보안 관리 > 사용자 관리 > 상태 변경

# 의심 계정의 API 토큰을 즉시 회수 (ADMIN 전용 — 아래 참고)
curl -X POST http://localhost/api/v5/admin/tokens/<TOKEN>/revoke \
-H "X-API-Key: <ADMIN_API_KEY>"
활성 «세션» 목록은 제품에 없습니다

로그인 세션을 나열하거나 강제로 끊는 화면도 API 도 없습니다. 세션은 만료를 기다리거나 서버를 재기동해야 끊깁니다. 즉시 막아야 한다면 계정을 비활성화하고 API 토큰을 회수하세요 — 그 둘이 지금 쓸 수 있는 수단입니다.

누가 언제 들어왔는지는 감사 로그 와 아래 2단계의 system.log grep 으로 봅니다.

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 으로 연락해 주세요.