Skip to main content

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.

LayerSecurity AreaPrimary Features
1. NetworkFirewall, Segmentation, VPNBlock external access, Network isolation
2. TransportHTTPS/TLS, EncryptionEncrypt communication data
3. ApplicationAuthentication, Authorization, Filters, SessionsUser authentication and permission management
4. DataDB Access Control, EncryptionProtect 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.

ItemValue
Issuerplantpulse-certs (Always running — (healthy) is normal)
Material LocationVolume pp-security, In-container path /var/security/plantpulse
ReaderData Lake and App — both :ro
ExceptionOnly plantpulse-plugin-opcua has read-write access. Because it must create a secure temp directory during startup
ConfigurationPP_TLS_* environment variable in compose
Certificates are not re-issued on restart

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​

VariableDefaultDescription
PP_TLS_ENABLEDtrueEnable TLS
PP_TLS_CERT_DIR/var/security/plantpulseCertificate directory
PP_TLS_DOMAINplantpulse.ioDefault domain
PP_TLS_KEYSTORE_PASSWORDkopens123! (Must change)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 (include NAT/external IP is mandatory)
PP_TLS_FORCE_REGENERATEfalseForce 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
If even one invalid IP is included in the SAN, «not a single» certificate will be generated

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.

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

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.

GroupCountNames
Current container names8localhost · plantpulse-datalake · plantpulse-server-web · plantpulse-batch-web · plantpulse-warehouse-web · plantpulse-plugin-opcua · plantpulse-plugin-aasx · plantpulse-proxy
Old names before renaming8plantpulse-warehouse · plantpulse-server · plantpulse-batch · plantpulse-warehouse-s3 · plantpulse-opcua · plantpulse-aasx · plantpulse-plugin-opcua-server · plantpulse-plugin-aasx-server
Worker nodes5plantpulse-datalake-worker-1 ~ -5
Worker names were added on 2026-09-09

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 excludeUrls are 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 _csrf parameter or X-CSRF-TOKEN header 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.

Does not overwrite existing certificates

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

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

MQTT TLS(1884) is not terminated by the proxy

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​

Usually not necessary for container-shipped builds

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 VersionStatusRecommendation
SSLv3DisablePOODLE vulnerability
TLS 1.0DisableVulnerabilities exist
TLS 1.1DisableVulnerabilities exist
TLS 1.2AllowedRecommended
TLS 1.3AllowedLatest, 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_key is the token value itself

/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>"
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 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.

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 headers

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": "..."}
This is the only response that is not an envelope

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.

HTTPPathDescription
GET/api/v5/tokenToken list — token values are masked (first 8 chars + ****)
GET/api/v5/token/{token}Single lookup — masked
POST/api/v5/tokenIssue
DELETE/api/v5/token/{token}Revoke — the performer is X-API-Key the owner
POST/api/v5/admin/tokens/{token}/revokeADMIN-only revoke — for revoking others' tokens
Why there are two revoke paths

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 FieldDescription
login_idAccount to receive the token. If omitted, issued for the caller (X-API-Key owner)
ipIP pattern allowed to use the token
descriptionPurpose memo
ttl_daysValidity period (days). If omitted, uses the global default(engine.api.token.default_ttl_days), if 0, permanent, if positive, that many days
Only ADMIN can issue tokens

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.

Why ttl_days was needed

If 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.

ItemRecommended ValueDescription
Minimum length8+ characters12+ characters recommended
Complexity3 or more types3 of uppercase/lowercase letters, numbers, special characters
Change period90 daysQuarterly change
History managementLast 3Prevent reuse of previous passwords
Consecutive failure lockout5 attemptsLock account after 5 consecutive failures
Unlock30 minutes or adminAuto unlock or manual unlock

HTTP Security Headers​

Configure HTTP response headers to enable 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​

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​

<!-- 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 theft via XSS
securetruePrevent cookies from being sent over HTTP (HTTPS required)
tracking-modeCOOKIEPrevent 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 AreaPurposeExample
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
  • 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 ItemVerification Method
Abnormal login attemptsCheck login failure counts in audit logs
System error logsCheck security-related error/warning logs
Service availabilityCheck health check API responses

Weekly Inspection​

Inspection ItemVerification Method
User account statusCheck for unused, inactive accounts
Permission change historyCheck permission changes in audit logs
Firewall logsCheck blocked access attempts
SSL certificate validityCheck certificate expiration dates

Monthly Inspection​

Inspection ItemVerification Method
OS security patchesCheck latest security updates applied
Password policy complianceCheck for passwords unchanged for a long time
DB access permissionsCheck access control settings such as pg_hba.conf
Backup data securityCheck backup file encryption and access permissions
Port scanCheck for unnecessarily open ports
TLS settingsCheck 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 active «session» list in the product

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.