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.
| Layer | Security Domain | Key Function |
|---|---|---|
| 1. Network | Firewall, Segmentation, VPN | Block external access, network isolation |
| 2. Transmission | HTTPS/TLS, Encryption | Encrypt communication data |
| 3. Application | Authentication, Authorization, Filters, Session | User authentication and access control |
| 4. Data | DB Access Control, Encryption | Protect 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.
| Item | Value |
|---|---|
| Generation by | plantpulse-certs (one-shot — Exited (0) is normal) |
| Material location | Volume pp-security, container path /var/security/plantpulse |
| Read by | Data Lake and App — both :ro |
| Exception | plantpulse-plugin-opcua-server only read-write. It must create a secure temp directory on startup |
| Configuration | PP_TLS_* environment variables in compose |
plantpulse-certsIt 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
| Variable | Default | Description |
|---|---|---|
PP_TLS_ENABLED | true | Enable TLS |
PP_TLS_CERT_DIR | /var/security/plantpulse | Certificate directory |
PP_TLS_DOMAIN | plantpulse.io | Base domain |
PP_TLS_KEYSTORE_PASSWORD | kopens123! (change required) | keystore password |
PP_TLS_TRUSTSTORE_PASSWORD | ${PP_TLS_KEYSTORE_PASSWORD} | truststore password |
PP_TLS_VALID_DAYS | 3650 | Validity period (days) |
PP_TLS_EC_GROUP | secp256r1 | ECDSA curve |
PP_TLS_SIGALG | SHA256withECDSA | Signature algorithm |
PP_TLS_OPCUA_APP_URI | urn:plantpulse:opcua:server | OPC-UA Application URI |
PP_TLS_NODE_NAMES | master worker-1 ... worker-5 | Cluster node name |
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 (must include NAT/external IP) |
PP_TLS_FORCE_REGENERATE | false | true 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
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.
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
excludeUrlscan 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
_csrfparameter orX-CSRF-TOKENheader. - 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.
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
| Item | Value |
|---|---|
| Volume | pp-proxy-certs |
| Container paths | /etc/nginx/certs/server.crt · /etc/nginx/certs/server.key |
| Self-signed CN | PP_PROXY_CERT_CN (default plantpulse.local) |
Place the certificate file as a chain including intermediate certificates (fullchain).
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
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 Version | Status | Recommendation |
|---|---|---|
| SSLv3 | Disabled | POODLE vulnerability |
| TLS 1.0 | Disabled | Vulnerabilities exist |
| TLS 1.1 | Disabled | Vulnerabilities exist |
| TLS 1.2 | Allowed | Recommended |
| TLS 1.3 | Allowed | Latest, 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/v5/auth does not create a new string. If the submitted token is valid, that token value is returned directly as api_key (APIManager — api_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>"
| Response | Meaning |
|---|---|
200 | Issuance successful |
400 | Body 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.
| Property | Default |
|---|---|
engine.api.v5.auth.bruteforce.enabled | true |
engine.api.v5.auth.bruteforce.ip_limit | 30 |
engine.api.v5.auth.bruteforce.window.seconds | 60 — takes precedence over window.minutes if present |
engine.api.v5.auth.bruteforce.window.minutes | 15 |
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": "..."}
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.
| HTTP | Path | Action |
|---|---|---|
GET | /api/v5/token | Token list — token value is masked (first 8 chars + ****) |
GET | /api/v5/token/{token} | Single fetch — masked |
POST | /api/v5/token | Issue |
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 Field | Description |
|---|---|
login_id | Account to receive the token. If omitted, issued to caller (X-API-Key owner) |
ip | IP pattern allowing token use |
description | Usage note |
ttl_days | Validity period (days). If omitted, use global default (engine.api.token.default_ttl_days), 0 for perpetual, positive number for that many days |
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.
ttl_days was neededIf 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.
Recommended Password Policy Values
| Item | Recommended Value | Description |
|---|---|---|
| Minimum length | 8+ characters | 12+ characters preferred |
| Complexity | 3+ types combined | Mix of uppercase, lowercase, digits, special characters |
| Change frequency | 90 days | Change quarterly |
| History management | Last 3 | Prevent reuse of previous passwords |
| Consecutive failure lock | 5 attempts | Lock after 5 failed attempts |
| Unlock | 30 minutes or admin | Auto or manual unlock |
HTTP Security Headers
HTTP response headers are set to activate web browser security features.
Recommended Security Headers
| Header | Value | Description |
|---|---|---|
X-XSS-Protection | 1; mode=block | Enable browser XSS filter |
X-Content-Type-Options | nosniff | Prevent MIME type sniffing |
X-Frame-Options | SAMEORIGIN | Prevent clickjacking |
Content-Security-Policy | default-src 'self' | Restrict content sources |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Enforce HTTPS (HSTS) |
Referrer-Policy | strict-origin-when-cross-origin | Restrict 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
Cookie Security Configuration
<!-- 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>
| Setting | Value | Description |
|---|---|---|
http-only | true | Prevent session hijacking via XSS |
secure | true | Prevent cookie transmission over HTTP (HTTPS required) |
tracking-mode | COOKIE | Prevent 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 Zone | Purpose | Example |
|---|---|---|
| OT Network | PLC, SCADA, Sensors | 10.0.1.0/24 |
| DMZ | PlantPulse Web Server, API | 10.0.2.0/24 |
| IT Network | User Access, Management | 10.0.3.0/24 |
| Data Network | DB, Messaging | 10.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 Item | Verification Method |
|---|---|
| Abnormal login attempts | Check failed login count in audit log |
| System error logs | Check for security-related errors/warnings |
| Service availability | Verify health check API response |
Weekly Inspection
| Inspection Item | Verification Method |
|---|---|
| User account status | Inspect unused and inactive accounts |
| Permission change history | Check permission changes in audit log |
| Firewall logs | Review blocked access attempts |
| SSL certificate expiry | Check certificate expiration date |
Monthly Inspection
| Inspection Item | Verification Method |
|---|---|
| OS security patches | Verify latest security updates applied |
| Password policy compliance | Check for long-unchanged passwords |
| DB access permissions | Review pg_hba.conf and access control settings |
| Backup data security | Verify backup encryption and access rights |
| Port scanning | Check for unnecessarily open ports |
| TLS configuration | Inspect 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.