Security Configuration
Overview
This document describes how to configure security settings for the PlantPulse platform. Through security configuration at the network, transport, application, and data levels, you can operate the platform securely.
Important: Since industrial control systems (ICS) prioritize availability, please verify security configuration changes in a test environment before applying them to production.
Security Architecture
The security of PlantPulse consists of four layers.
| Layer | Security Area | Primary Features |
|---|---|---|
| 1. Network | Firewall, Segmentation, VPN | Block external access, Network isolation |
| 2. Transport | HTTPS/TLS, Encryption | Encrypt communication data |
| 3. Application | Authentication, Authorization, Filters, Sessions | User authentication and permission management |
| 4. Data | DB Access Control, Encryption | Protect stored data |
TLS / Certificates (Automatic Management)
The issuer of certificates is plantpulse-certs a single container. It runs first when the stack starts, creates all materials using pd_ssl_prepare(plantpulse-datalake-cli of lib/lib-ssl.sh), and writes them to the pp-security volume. Then, while running, it checks expiration every 6 hours and re-issues only when the expiration is within 30 days. The other containers mount that volume read-only and use it.
| Item | Value |
|---|---|
| Issuer | plantpulse-certs (Always running — (healthy) is normal) |
| Material Location | Volume pp-security, In-container path /var/security/plantpulse |
| Reader | Data Lake and App — both :ro |
| Exception | Only plantpulse-plugin-opcua has read-write access. Because it must create a secure temp directory during startup |
| Configuration | PP_TLS_* environment variable in compose |
pd_ssl_prepare re-issues only when the requested SAN is different from what is recorded in .certs_generated. Therefore,
restarting the container does not change the materials. If re-issuance is needed, follow the
procedure below explicitly.
restart: unless-stopped — if a person intentionally stops it, restarting the Docker daemon will leave it stopped.
This preserves any materials that were manually placed during incident response.
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 | Default domain |
PP_TLS_KEYSTORE_PASSWORD | kopens123! (Must change) | 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 (include NAT/external IP is mandatory) |
PP_TLS_FORCE_REGENERATE | false | Force regeneration if true |
Certificate Generation Procedure
This is performed automatically once during installation, so normally nothing needs to be done. Only when the SAN has changed or the certificate has expired/corrupted, perform the following.
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 an invalid format, openssl rejects the entire extension file. Do not fill in DOCKER_PP_EXTERNAL_IP from a box that is not NAT.
Nowhere in the logs will there be any mention of certificates. The default value of PP_TLS_SAN_DNS is twenty-one, in three groups.
| Group | Count | Names |
|---|---|---|
| Current container names | 8 | localhost · plantpulse-datalake · plantpulse-server-web · plantpulse-batch-web · plantpulse-warehouse-web · plantpulse-plugin-opcua · plantpulse-plugin-aasx · plantpulse-proxy |
| Old names before renaming | 8 | plantpulse-warehouse · plantpulse-server · plantpulse-batch · plantpulse-warehouse-s3 · plantpulse-opcua · plantpulse-aasx · plantpulse-plugin-opcua-server · plantpulse-plugin-aasx-server |
| Worker nodes | 5 | plantpulse-datalake-worker-1 ~ -5 |
Certificates issued before that date contained no workers at all, and worker IPs were also not in IP SAN (dev 41 measurement: 0 workers out of 16 DNS names). So terminating TLS on workers meant the client had no name or address to validate. Clusters using certificates issued before that date need re-issuance.
If you have changed container names or directly specified the backend host, you must also update the SAN list.
If you have expanded to six or more workers, you must also add those names to PP_TLS_SAN_DNS —
the default is up to five.
:::
Output generated by 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/ # 클라이언트 인증서 (선택)
Using External CA Certificates
To use company / Let's Encrypt CA certificates instead of self-signed ones:
# 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
Expiration monitoring: Please set up external monitoring (
openssl x509 -enddate) to generate an alarm 30 days before certificate expiration.
Web Security Filters
PlantPulse protects web requests through security filters defined in web.xml.
SecurityFilter
It is the core filter that blocks access from unauthenticated users.
<!-- 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 for all requests (
/*). - Paths specified in
excludeUrlsare accessible without authentication. - Unauthenticated requests are redirected to the login page.
XSSFilter
A 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 Defense
Performs token-based validation 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 tokens for POST, PUT, DELETE requests.
- The
_csrfparameter orX-CSRF-TOKENheader is required for form requests. - REST API (
/api/v5/) uses token authentication, so it is excluded from the CSRF filter.
EncodingFilter
Unifies 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
80 / 443 that users see is terminated by plantpulse-proxy(nginx). The web server container does not open host ports, so the place to change browser-facing certificates is the proxy, not Tomcat.
Proxy Certificate — Self-Signed by Default
Right after installation, 443 is open without any preparation. This is because the proxy creates a self-signed certificate on first startup and stores it in the pp-proxy-certs volume.
If a customer's real certificate is replaced by a self-signed one on restart, that would be a silent incident, so if the file already exists, the proxy uses it as is.
Replacing with a Real Certificate
There is no need to modify the nginx configuration — the path is fixed, so you only need to replace 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 |
| In-container path | /etc/nginx/certs/server.crt · /etc/nginx/certs/server.key |
| Self-signed CN | PP_PROXY_CERT_CN (default plantpulse.local) |
Please provide the certificate file as a chain including intermediate certificates (fullchain).
The proxy passes 1884 through as passthrough, and the broker terminates TLS. Therefore, changing the proxy certificate does not change the certificate that MQTT clients validate — those use the materials created 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 standard configurations, you only need to change the proxy certificate mentioned above. The TLS materials for the backend section within the container are already created by plantpulse-certs.
To use HTTPS, you need to 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" />
Redirecting from HTTP to HTTPS
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, we recommend allowing only TLS 1.2 or higher.
| TLS Version | Status | Recommendation |
|---|---|---|
| SSLv3 | Disable | POODLE vulnerability |
| TLS 1.0 | Disable | Vulnerabilities exist |
| TLS 1.1 | Disable | 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 sending api_key in the header. There are two paths to obtain
an api_key, and the subsequent calling method is the same.
/api/v5/auth does not generate a new string. If the submitted token is valid, that token value is returned
as-is as the api_key (APIManager — api_key = token.getToken()). The token is a UUID
format, not a JWT.
Token Authentication (External Integration · Automation)
Receive an api_key with a pre-issued API token. This is the path used for production integrations.
# 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 returned together |
Brute-force defense is one type of IP-based counter. Each time a failure occurs, the counter for that IP increments,
and if the limit is exceeded within the window, it is blocked with 429. Adjust it 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 the first priority, and if absent, Authorization: Bearer <api_key> is read as fallback.
The Bearer fallback is a standard path for MCP clients that can only send that header, and the value is the same
api_key. Schemes other than Bearer are ignored.
ID/Password Authentication (Console SPA Only)
Log in with ID/password to receive an api_key. This is the path used by the console UI, and for server-to-server integration, please use token authentication — it would send plaintext credentials every time and widen 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 the {data, meta, errors} format, but /api/v5/auth/login returns plain JSON for both success and failure.
If you are sharing a common parser, please branch this path only.
expires_at is epoch milliseconds, and if it's a permanent token, it's null.
Token Issuance/Revocation API (/api/v5/token)
Does the same thing as the console System > API Token(/token/index) screen via REST. Use it when creating and deleting
tokens in automated wiring scripts. For screen usage, see
Security Management.
| HTTP | Path | Description |
|---|---|---|
GET | /api/v5/token | Token list — token values are masked (first 8 chars + ****) |
GET | /api/v5/token/{token} | Single lookup — masked |
POST | /api/v5/token | Issue |
DELETE | /api/v5/token/{token} | Revoke — the performer is X-API-Key the owner |
POST | /api/v5/admin/tokens/{token}/revoke | ADMIN-only revoke — for revoking others' tokens |
DELETE /api/v5/token/{token} is for cleaning up your own tokens, and
POST /api/v5/admin/tokens/{token}/revoke is for an admin to revoke someone else's tokens.
The latter only passes with the ADMIN role — even if called with a API role token, it returns 403.
The latter is what you use during incident response.
Both log revoked_at and invalidate the token cache, so they are blocked immediately.
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 for the caller (X-API-Key owner) |
ip | IP pattern allowed to use the token |
description | Purpose memo |
ttl_days | Validity period (days). If omitted, uses the global default(engine.api.token.default_ttl_days), if 0, permanent, if positive, that many days |
If the role of the requesting account (X-API-Key owner) is not ADMIN, it returns 403 E1200 and 1 entry
of TOKEN_ISSUE_DENIED is left in the audit log. This is to prevent an API role account from escalating privileges by
issuing tokens for itself or others. The role of the target account (login_id) is irrelevant.
ttl_days does not expand this capability.
ttl_days was neededIf you change the global default to 0 (permanent), all tokens created by customers, even from the UI, become permanent.
This is to prevent people from modifying the global setting just to make one token for automated wiring permanent.
Negative or non-integer values result in 400 VALIDATION_FAILED — because silently swallowing an invalid value and creating
a permanent token is more dangerous.
On successful issuance, the token plaintext is included once. It does not appear again in listings or lookups, so
save it at this time. Issuance is recorded in the audit log as TOKEN_ISSUED, and permanent tokens are recorded as PERMANENT
in the expiration field.
Password Security
Password Hashing
PlantPulse stores user passwords as SHA-256 hashes. The original password is not stored on the server.
Recommended Password Policy
| Item | Recommended Value | Description |
|---|---|---|
| Minimum length | 8+ characters | 12+ characters recommended |
| Complexity | 3 or more types | 3 of uppercase/lowercase letters, numbers, special characters |
| Change period | 90 days | Quarterly change |
| History management | Last 3 | Prevent reuse of previous passwords |
| Consecutive failure lockout | 5 attempts | Lock account after 5 consecutive failures |
| Unlock | 30 minutes or admin | Auto unlock or manual unlock |
HTTP Security Headers
Configure HTTP response headers to enable 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
If 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 exposure.
<!-- 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 Settings
<!-- 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 theft via XSS |
secure | true | Prevent cookies from being sent over HTTP (HTTPS required) |
tracking-mode | COOKIE | Prevent session ID exposure in URLs |
Session Fixation Prevention
PlantPulse regenerates the session ID upon successful login to prevent Session Fixation attacks. This feature is built into SecurityFilter and is applied automatically without additional configuration.
Database Security
PostgreSQL
Set the allowed access ranges 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 문만 로깅
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
Network Security
Firewall Configuration
Please allow only the ports required for 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, we recommend separating the network as follows.
| Network Area | 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 |
- Please ensure the OT network and IT network do not communicate directly.
- Place the PlantPulse server in the DMZ, allowing only limited communication with both networks.
- Place the database in the data network, and only allow access from the PlantPulse server.
VPN Access
If remote access to the management console is required, configure access via VPN.
- Please allow access to the management console and SSH only through VPN.
- Issue VPN accounts individually, and do not use shared accounts.
- Record VPN access logs and review them periodically.
Security Inspection Checklist
Daily Inspection
| Inspection Item | Verification Method |
|---|---|
| Abnormal login attempts | Check login failure counts in audit logs |
| System error logs | Check security-related error/warning logs |
| Service availability | Check health check API responses |
Weekly Inspection
| Inspection Item | Verification Method |
|---|---|
| User account status | Check for unused, inactive accounts |
| Permission change history | Check permission changes in audit logs |
| Firewall logs | Check blocked access attempts |
| SSL certificate validity | Check certificate expiration dates |
Monthly Inspection
| Inspection Item | Verification Method |
|---|---|
| OS security patches | Check latest security updates applied |
| Password policy compliance | Check for passwords unchanged for a long time |
| DB access permissions | Check access control settings such as pg_hba.conf |
| Backup data security | Check backup file encryption and access permissions |
| Port scan | Check for unnecessarily open ports |
| TLS settings | Check for weak protocols/cipher suites |
Security Incident Response
If a security incident has occurred or is suspected, respond according to the following procedure.
Stage 1: Detection and Isolation
# 의심 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>"
There is no UI or API to list login sessions or force-disconnect them. Sessions are only terminated by expiration or by restarting the server. If you need to block immediately, disable the account and revoke the API token — those are the tools available now.
To see who logged in and when, refer to the Audit Log and the system.log grep
commands in Stage 2 below.
Stage 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
Stage 3: Recovery
- Change the password of the compromised account.
- Verify the integrity of affected systems.
- Restore data from backups if necessary.
Stage 4: Post-Incident Actions
- Write an incident report.
- Establish measures to prevent recurrence.
- Strengthen security settings.
- Conduct security training for relevant personnel.
Contact: For security-related inquiries, please contact webmaster@kopens.com.