Skip to main content

Security Configuration

Overview

This document describes how to configure security in the PlantPulse Platform. You can operate the platform securely by implementing security settings at the network, transmission, application, and data levels.

Important: Because availability is the top priority in industrial control systems (ICS), always validate security configuration changes in a test environment before applying them to production.

Security Architecture

PlantPulse security consists of four layers.

LayerSecurity DomainKey Function
1. NetworkFirewall, Segmentation, VPNBlock external access, network isolation
2. TransmissionHTTPS/TLS, EncryptionEncrypt communication data
3. ApplicationAuthentication, Authorization, Filters, SessionUser authentication and access control
4. DataDB Access Control, EncryptionProtect data at rest

TLS / Certificates (Auto-managed)

Certificate generation is plantpulse-certs one-shot container. It runs first when the stack starts up, generates all materials using prepare-ssl.sh, writes to the pp-security volume, then exits by itself. Other containers mount that volume as read-only and use it.

ItemValue
Generation byplantpulse-certs (one-shot — Exited (0) is normal)
Material locationVolume pp-security, container path /var/security/plantpulse
Read byData Lake and App — both :ro
Exceptionplantpulse-plugin-opcua-server only read-write. It must create a secure temp directory on startup
ConfigurationPP_TLS_* environment variables in compose
Do not restart plantpulse-certs

It is not a mistake to declare restart: "no". If you restart it, it will regenerate the certificate. Only perform reissuance explicitly using the procedure below when necessary.

This container has health checks disabled (healthcheck: disable), so judge only by exit code. Downstream containers also wait for «normal shutdown»(service_completed_successfully), not health.

env.sh TLS Variables

VariableDefaultDescription
PP_TLS_ENABLEDtrueEnable TLS
PP_TLS_CERT_DIR/var/security/plantpulseCertificate directory
PP_TLS_DOMAINplantpulse.ioBase domain
PP_TLS_KEYSTORE_PASSWORDkopens123! (change required)keystore password
PP_TLS_TRUSTSTORE_PASSWORD${PP_TLS_KEYSTORE_PASSWORD}truststore password
PP_TLS_VALID_DAYS3650Validity period (days)
PP_TLS_EC_GROUPsecp256r1ECDSA curve
PP_TLS_SIGALGSHA256withECDSASignature algorithm
PP_TLS_OPCUA_APP_URIurn:plantpulse:opcua:serverOPC-UA Application URI
PP_TLS_NODE_NAMESmaster worker-1 ... worker-5Cluster node name
PP_TLS_SAN_DNSlocalhost,<hostname>,<domain>SAN DNS
PP_TLS_SAN_IPS<HOST_IP>,<SERVICE_IP>,<PUBLIC_IP>,127.0.0.1SAN IP (must include NAT/external IP)
PP_TLS_FORCE_REGENERATEfalsetrue to force regeneration

Certificate Generation Procedure

This is automatically performed once during installation, so normally you do nothing. Only perform the following if SAN has changed or if the certificate has expired or been damaged.

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
If any invalid IP is mixed into SAN, not even one certificate will be created

PP_TLS_SAN_IPS is assembled from PP_HOST_IP · PP_SERVICE_IP · PP_MASTER_IP · PP_PUBLIC_IP · 127.0.0.1. If even one value has incorrect format, openssl will reject the entire extension file. Do not populate DOCKER_PP_EXTERNAL_IP from a box that is not behind NAT.

If the name the app uses to connect to the backend is not in SAN, it appears only as «TimeoutException»

The log contains no mention of certificates anywhere. compose adds both eight container names (localhost · plantpulse-datalake · plantpulse-server-web · plantpulse-batch-web · plantpulse-warehouse · plantpulse-plugin-opcua-server · plantpulse-plugin-aasx-server · plantpulse-proxy) and five old names before renaming to the SAN DNS list. If you change container names or specify the backend host directly, you must also update the SAN list.

Output produced by 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/ # 클라이언트 인증서 (선택)

Using External CA Certificates

To use a company or Let's Encrypt CA certificate instead of self-signed:

# 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

Expiry Monitoring: Set up external monitoring (openssl x509 -enddate) to trigger an alarm 30 days before certificate expiration.


Web Security Filters

PlantPulse protects web requests through security filters defined in web.xml.

SecurityFilter

Core filter that blocks unauthenticated users from accessing resources.

<!-- WEB-INF/web.xml inside the webapp (bundled in the WAR — reset on deploy, so change it in the source) -->
<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>
  • Checks authentication status for all requests (/*).
  • Paths specified in excludeUrls can be accessed without authentication.
  • Unauthenticated requests are redirected to the login page.

XSSFilter

Filter that prevents Cross-Site Scripting (XSS) attacks.

<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>
  • Removes dangerous tags such as <script>, <iframe> from request parameters.
  • API paths can be excluded from the filter because they apply separate input validation.

CSRF Protection

Token-based validation is performed to prevent Cross-Site Request Forgery (CSRF) attacks.

<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>
  • Validates CSRF token for POST, PUT, DELETE requests.
  • Form requests require the _csrf parameter or X-CSRF-TOKEN header.
  • REST API (/api/v5/) uses token authentication, so it is excluded from CSRF filtering.

EncodingFilter

Standardizes character encoding to prevent encoding-related security vulnerabilities.

<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 Configuration

The external-facing ports 80 / 443 are terminated by plantpulse-proxy (nginx). The web server container does not expose host ports, so the place to change the browser-facing certificate is the proxy, not Tomcat.

Proxy Certificate — Default is Self-signed

Right after installation, port 443 opens immediately without any preparation. The proxy creates a self-signed certificate on first startup and puts it in the pp-proxy-certs volume.

Existing certificates are not overwritten

Restarts reverting a customer's actual certificate to self-signed would be a silent incident, so the proxy reuses the file if it already exists.

Replace with Actual Certificate

No nginx configuration changes are needed — the path is fixed, so just swap the files.

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
ItemValue
Volumepp-proxy-certs
Container paths/etc/nginx/certs/server.crt · /etc/nginx/certs/server.key
Self-signed CNPP_PROXY_CERT_CN (default plantpulse.local)

Place the certificate file as a chain including intermediate certificates (fullchain).

MQTT TLS (1884) is not terminated by the proxy

The proxy passes through port 1884 and the broker terminates TLS. So changing the proxy certificate does not change the certificate that MQTT clients validate — that side uses materials generated by plantpulse-certs.

(Reference) Attaching Certificates Directly to the Web Server

Usually not needed in containerized deployments

The following is for reference for environments where the web server is directly exposed without a proxy. In the standard configuration, changing only the proxy certificate above is sufficient. The TLS materials for the backend segment inside the container are already generated by the plantpulse-certs one-shot.

To use HTTPS, you must install an SSL certificate.

# 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 Connector

Add an HTTPS connector to Tomcat's server.xml.

<!-- plantpulse-server/server/conf/server.xml -->
<!-- HTTP connector (redirects to HTTPS) -->
<Connector port="80" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="443" />

<!-- HTTPS connector -->
<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 to HTTPS Redirect

Automatically redirect all HTTP requests to HTTPS.

<!-- WEB-INF/web.xml inside the webapp (bundled in the WAR — reset on deploy, so change it in the source) -->
<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 Version Management

For security, it is recommended to allow only TLS 1.2 and above.

TLS VersionStatusRecommendation
SSLv3DisabledPOODLE vulnerability
TLS 1.0DisabledVulnerabilities exist
TLS 1.1DisabledVulnerabilities exist
TLS 1.2AllowedRecommended
TLS 1.3AllowedLatest, most secure

API Authentication

The PlantPulse REST API (/api/v5) is called by placing api_key in the header. There are two paths to obtaining api_key, and the calling method afterwards is the same.

api_key is the token value itself

/api/v5/auth does not create a new string. If the submitted token is valid, that token value is returned directly as api_key (APIManagerapi_key = token.getToken()). Tokens are in UUID format, not JWT.

Token Authentication (External Integration · Automation)

Obtain api_key with a pre-issued API token. This is the path used for operational integration.

# 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>"
ResponseMeaning
200Issuance successful
400Body is not JSON or token is missing
401 (E1100)Token is invalid
429 (E1101)Brute-force blocked. Retry-After header is included

Brute-force protection uses one IP-based counter. The counter for that IP increases with each failure, and if the limit is exceeded within the window, the request is blocked with 429. Adjust in plantpulse-engine.properties.

PropertyDefault
engine.api.v5.auth.bruteforce.enabledtrue
engine.api.v5.auth.bruteforce.ip_limit30
engine.api.v5.auth.bruteforce.window.seconds60 — takes precedence over window.minutes if present
engine.api.v5.auth.bruteforce.window.minutes15
Two header options

X-API-Key is first priority; if absent, Authorization: Bearer <api_key> is used as fallback. Bearer fallback is the standard path for MCP clients that cannot send any other header, and the value is the same api_key. Schemes other than Bearer are ignored.

ID/Password Authentication (Console SPA Only)

Obtain api_key by logging in with ID and password. This path is used by the console UI, and use token authentication for server-to-server integration — otherwise you send plaintext credentials each time, widening the brute-force surface.

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": "..."}
This response alone has no envelope

Other V5 endpoints use {data, meta, errors} format, but /api/v5/auth/login returns plain JSON for both success and failure. If you use a shared parser, split this path. expires_at is epoch milliseconds, and null for perpetual tokens.

Token Issue·Revoke API (/api/v5/token)

REST equivalent of the console System > API Tokens (/token/index) screen. Use this in auto-provisioning scripts to create and delete tokens. For UI usage, see Security Management.

HTTPPathAction
GET/api/v5/tokenToken list — token value is masked (first 8 chars + ****)
GET/api/v5/token/{token}Single fetch — masked
POST/api/v5/tokenIssue
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}'
Body FieldDescription
login_idAccount to receive the token. If omitted, issued to caller (X-API-Key owner)
ipIP pattern allowing token use
descriptionUsage note
ttl_daysValidity period (days). If omitted, use global default (engine.api.token.default_ttl_days), 0 for perpetual, positive number for that many days
Only ADMIN can issue tokens

If the requesting account's (X-API-Key owner's) role is not ADMIN, returns 403 E1200 and logs TOKEN_ISSUE_DENIED once in audit log. This prevents API role accounts from escalating privileges by issuing their own or others' tokens. The target account's (login_id) role is irrelevant. ttl_days does not broaden this restriction.

Why ttl_days was needed

If you change the global default to 0 (perpetual), all tokens created by customers on the screen also become perpetual. This field exists to avoid touching the global setting just to keep one provisioning token perpetual. Negative or non-integer values return 400 VALIDATION_FAILED — silently accepting a strange value and becoming a perpetual token is more dangerous.

The successful issue response includes the token plaintext once. It does not appear again in list or fetch, so save it at this time. Issue is logged in audit as TOKEN_ISSUED, and perpetual tokens show PERMANENT in the expiry field.


Password Security

Password Hashing

PlantPulse stores user passwords as SHA-256 hashes. The original password is never stored on the server.

ItemRecommended ValueDescription
Minimum length8+ characters12+ characters preferred
Complexity3+ types combinedMix of uppercase, lowercase, digits, special characters
Change frequency90 daysChange quarterly
History managementLast 3Prevent reuse of previous passwords
Consecutive failure lock5 attemptsLock after 5 failed attempts
Unlock30 minutes or adminAuto or manual unlock

HTTP Security Headers

HTTP response headers are set to activate web browser security features.

HeaderValueDescription
X-XSS-Protection1; mode=blockEnable browser XSS filter
X-Content-Type-OptionsnosniffPrevent MIME type sniffing
X-Frame-OptionsSAMEORIGINPrevent clickjacking
Content-Security-Policydefault-src 'self'Restrict content sources
Strict-Transport-Securitymax-age=31536000; includeSubDomainsEnforce HTTPS (HSTS)
Referrer-Policystrict-origin-when-cross-originRestrict referrer information

Nginx Configuration Example

When using Nginx as a reverse proxy, add security headers as follows.

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 Method Restriction

Block unnecessary HTTP methods to prevent information disclosure.

<!-- WEB-INF/web.xml inside the webapp (bundled in the WAR — reset on deploy, so change it in the source) -->
<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>

Note: The OPTIONS method is used for CORS preflight requests. If API integration with external systems is required, you may need to allow the OPTIONS method.


Session Security

<!-- WEB-INF/web.xml inside the webapp (bundled in the WAR — reset on deploy, so change it in the source) -->
<session-config>
<session-timeout>30</session-timeout>
<cookie-config>
<http-only>true</http-only> <!-- block cookie access from JavaScript -->
<secure>true</secure> <!-- send cookies over HTTPS only -->
<max-age>1800</max-age> <!-- cookie lifetime (seconds) -->
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
SettingValueDescription
http-onlytruePrevent session hijacking via XSS
securetruePrevent cookie transmission over HTTP (HTTPS required)
tracking-modeCOOKIEPrevent session ID exposure in URLs

Session Fixation Prevention

PlantPulse regenerates the session ID after successful login to prevent Session Fixation attacks. This feature is built into SecurityFilter and is automatically applied without additional configuration.


Database Security

PostgreSQL

Set the access allowlist in the pg_hba.conf file.

# /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

Additional security settings (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

Network Security

Firewall Configuration

Allow only the ports requiring external access in the firewall for 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

Network Segmentation

In industrial environments, it is recommended to separate networks as follows.

Network ZonePurposeExample
OT NetworkPLC, SCADA, Sensors10.0.1.0/24
DMZPlantPulse Web Server, API10.0.2.0/24
IT NetworkUser Access, Management10.0.3.0/24
Data NetworkDB, Messaging10.0.4.0/24
  • Ensure OT and IT networks do not communicate directly.
  • Place the PlantPulse server in the DMZ and allow only restricted communication with both networks.
  • Place the database in the data network and allow access only from the PlantPulse server.

VPN Access

When remote access to the management console is required, configure access through VPN.

  • Always require VPN for management console and SSH access.
  • Issue VPN accounts individually; do not share accounts.
  • Log VPN access and review periodically.

Security Inspection Checklist

Daily Inspection

Inspection ItemVerification Method
Abnormal login attemptsCheck failed login count in audit log
System error logsCheck for security-related errors/warnings
Service availabilityVerify health check API response

Weekly Inspection

Inspection ItemVerification Method
User account statusInspect unused and inactive accounts
Permission change historyCheck permission changes in audit log
Firewall logsReview blocked access attempts
SSL certificate expiryCheck certificate expiration date

Monthly Inspection

Inspection ItemVerification Method
OS security patchesVerify latest security updates applied
Password policy complianceCheck for long-unchanged passwords
DB access permissionsReview pg_hba.conf and access control settings
Backup data securityVerify backup encryption and access rights
Port scanningCheck for unnecessarily open ports
TLS configurationInspect for weak protocols/cipher suites

Security Incident Response

Follow the procedure below if a security incident occurs or is suspected.

Step 1: Detection and Isolation

# 의심 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>"

Step 2: Analysis

# 로그인 이력 조회
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

Step 3: Recovery

  • Change passwords for compromised accounts.
  • Verify the integrity of affected systems.
  • Restore data from backups if necessary.

Step 4: Post-Incident Actions

  • Prepare an incident report.
  • Establish measures to prevent recurrence.
  • Strengthen security settings.
  • Conduct security training for relevant personnel.

Contact: For security inquiries, please contact webmaster@kopens.com.