> For the complete documentation index, see [llms.txt](https://kopens.gitbook.io/plantpulse-platform/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://kopens.gitbook.io/plantpulse-platform/admin/security.md).

# 보안 설정

## 개요

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

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

## 보안 아키텍처 <a href="#security-architecture" id="security-architecture"></a>

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

```mermaid
graph TB
  subgraph L1["1. 네트워크 계층"]
    FW[방화벽 / firewalld · ufw]
    VPN[VPN / Tailscale CGN]
    SEG[네트워크 세그멘테이션<br/>외부 / DMZ / 사설]
  end
  subgraph L2["2. 전송 계층 (TLS)"]
    HTTPS[HTTPS :443 / :7443]
    MQTT_TLS[MQTT TLS :1884]
    KAFKA_TLS[Kafka SSL :9094]
    OPCUA_TLS[OPC-UA TLS :11005]
    CASS_TLS[Cassandra Internode TLS]
  end
  subgraph L3["3. 애플리케이션 계층"]
    AUTH[로그인 인증<br/>비밀번호 정책]
    RBAC[역할 기반 권한<br/>RBAC]
    FILTER[웹 필터<br/>XSS · CSRF · CSP]
    API[API Key · Bearer Token]
    AUDIT[감사 로그]
  end
  subgraph L4["4. 데이터 계층"]
    DB_AUTH[DB 인증 · ACL]
    SECRET[Secret 관리<br/>env.sh · vault]
    BACKUP_ENC[백업 암호화]
    DISK[디스크 LUKS]
  end

  L1 --> L2 --> L3 --> L4
```

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

***

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

PlantPulse 는 `prepare-ssl.sh` 가 **단일 명령으로** 모든 모듈의 인증서를 생성·배포합니다. 모든 설정은 `plantpulse-startup/env.sh` 의 `PP_TLS_*` 변수에서 일괄 관리됩니다.

### env.sh TLS 변수

| 변수                           | 기본                                             | 설명                       |
| ---------------------------- | ---------------------------------------------- | ------------------------ |
| `PP_TLS_ENABLED`             | `true`                                         | TLS 활성화                  |
| `PP_TLS_CERT_DIR`            | `/var/security/plantpulse`                     | 인증서 디렉토리                 |
| `PP_TLS_DOMAIN`              | `plantpulse.io`                                | 기본 도메인                   |
| `PP_TLS_KEYSTORE_PASSWORD`   | `kopens123!` (변경 필수)                           | keystore 비밀번호            |
| `PP_TLS_TRUSTSTORE_PASSWORD` | `${PP_TLS_KEYSTORE_PASSWORD}`                  | truststore 비밀번호          |
| `PP_TLS_VALID_DAYS`          | `3650`                                         | 유효 기간 (일)                |
| `PP_TLS_EC_GROUP`            | `secp256r1`                                    | ECDSA 곡선                 |
| `PP_TLS_SIGALG`              | `SHA256withECDSA`                              | 서명 알고리즘                  |
| `PP_TLS_OPCUA_APP_URI`       | `urn:plantpulse:opcua:server`                  | OPC-UA Application URI   |
| `PP_TLS_NODE_NAMES`          | `master worker-1 ... worker-5`                 | 클러스터 노드명                 |
| `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 (NAT/외부 IP 포함 필수) |
| `PP_TLS_FORCE_REGENERATE`    | `false`                                        | true 면 강제 재생성            |

### 인증서 생성 절차

```bash
cd /opt/kopens/plantpulse-platform/plantpulse-startup

# 1. env.sh 의 PP_TLS_* 검토 (외부 노출 IP/도메인 포함)
vi env.sh

# 2. 인증서 생성 (최초 또는 SAN 변경 시)
./prepare-ssl.sh

# 3. 강제 재생성 (만료 / 손상 시)
PP_TLS_FORCE_REGENERATE=true ./prepare-ssl.sh

# 4. 모듈 재시작
./restart.sh
```

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

### 외부 CA 인증서 사용

자체 서명 대신 회사 / Let's Encrypt CA 인증서를 사용하려면:

```bash
# 1. CA 발급 인증서를 JKS keystore 로 변환
keytool -importcert -alias rootca -file company-ca.pem \
  -keystore /var/security/plantpulse/server.jks \
  -storepass ${PP_TLS_KEYSTORE_PASSWORD}

keytool -importkeystore -srckeystore company.p12 -srcstoretype PKCS12 \
  -destkeystore /var/security/plantpulse/server.jks \
  -deststoretype JKS -deststorepass ${PP_TLS_KEYSTORE_PASSWORD}

# 2. PP_TLS_FORCE_REGENERATE 가 false 인 상태로 재시작
./restart.sh
```

> **만료 모니터링**: 인증서 만료 30일 전에 알람이 발생하도록 외부 모니터링 (`openssl x509 -enddate`) 을 설정해 주세요.

***

## 웹 보안 필터 <a href="#web-security-filters" id="web-security-filters"></a>

PlantPulse는 `web.xml`에 정의된 보안 필터를 통해 웹 요청을 보호합니다.

### SecurityFilter <a href="#security-filter" id="security-filter"></a>

인증되지 않은 사용자의 접근을 차단하는 핵심 필터입니다.

```xml
<!-- plantpulse-server/app/plantpulse-server-web/WEB-INF/web.xml -->
<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/v3/ping,/login,/css/,/js/,/images/</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>SecurityFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
```

* 모든 요청(`/*`)에 대해 인증 여부를 확인합니다.
* `excludeUrls`에 지정된 경로는 인증 없이 접근이 가능합니다.
* 인증되지 않은 요청은 로그인 페이지로 리다이렉트됩니다.

### XSSFilter <a href="#xss-filter" id="xss-filter"></a>

Cross-Site Scripting(XSS) 공격을 방지하는 필터입니다.

```xml
<filter>
    <filter-name>XSSFilter</filter-name>
    <filter-class>com.kopens.plantpulse.server.web.filter.XSSFilter</filter-class>
    <init-param>
        <param-name>excludeUrls</param-name>
        <param-value>/api/</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>XSSFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
```

* 요청 파라미터에서 `<script>`, `<iframe>` 등 위험한 태그를 제거합니다.
* API 경로는 별도의 입력 검증을 적용하므로 필터에서 제외할 수 있습니다.

### CSRF 방어 <a href="#csrf" id="csrf"></a>

Cross-Site Request Forgery(CSRF) 공격을 방지하기 위해 토큰 기반 검증을 수행합니다.

```xml
<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/v3/</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CSRFFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
```

* POST, PUT, DELETE 요청에 대해 CSRF 토큰을 검증합니다.
* 폼 요청 시 `_csrf` 파라미터 또는 `X-CSRF-TOKEN` 헤더가 필요합니다.
* REST API(`/api/v3/`)는 토큰 인증을 사용하므로 CSRF 필터에서 제외됩니다.

### EncodingFilter <a href="#encoding-filter" id="encoding-filter"></a>

문자 인코딩을 통일하여 인코딩 관련 보안 취약점을 방지합니다.

```xml
<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 설정 <a href="#https" id="https"></a>

### SSL 인증서 설치 <a href="#ssl-certificate" id="ssl-certificate"></a>

HTTPS를 사용하려면 SSL 인증서를 설치해야 합니다.

```bash
# 1. 키스토어 생성 (자체 서명 인증서 - 테스트용)
keytool -genkeypair -alias plantpulse -keyalg RSA -keysize 2048 \
    -validity 365 -keystore /opt/kopens/plantpulse-platform/plantpulse-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/conf/keystore.jks \
    -storepass changeit

# 3. 중간 인증서(Chain) 가져오기
keytool -importcert -alias intermediate -file /path/to/intermediate.crt \
    -keystore /opt/kopens/plantpulse-platform/plantpulse-server/conf/keystore.jks \
    -storepass changeit

# 4. 인증서 확인
keytool -list -v -keystore /opt/kopens/plantpulse-platform/plantpulse-server/conf/keystore.jks \
    -storepass changeit
```

### Tomcat HTTPS 커넥터 <a href="#tomcat-https-connector" id="tomcat-https-connector"></a>

Tomcat의 `server.xml`에 HTTPS 커넥터를 추가합니다.

```xml
<!-- plantpulse-server/conf/server.xml -->
<!-- HTTP 커넥터 (HTTPS 리다이렉트용) -->
<Connector port="80" protocol="HTTP/1.1"
           connectionTimeout="20000"
           redirectPort="443" />

<!-- HTTPS 커넥터 -->
<Connector port="443" protocol="org.apache.coyote.http11.Http11NioProtocol"
           maxThreads="200"
           SSLEnabled="true"
           scheme="https"
           secure="true"
           keystoreFile="/opt/kopens/plantpulse-platform/plantpulse-server/conf/keystore.jks"
           keystorePass="changeit"
           clientAuth="false"
           sslProtocol="TLSv1.2"
           sslEnabledProtocols="TLSv1.2,TLSv1.3"
           ciphers="TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" />
```

### HTTP에서 HTTPS로 리다이렉트 <a href="#https-redirect" id="https-redirect"></a>

모든 HTTP 요청을 자동으로 HTTPS로 리다이렉트합니다.

```xml
<!-- plantpulse-server/app/plantpulse-server-web/WEB-INF/web.xml -->
<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 버전 관리 <a href="#tls-version" id="tls-version"></a>

보안을 위해 TLS 1.2 이상만 허용하는 것을 권장합니다.

| TLS 버전  | 상태    | 권장         |
| ------- | ----- | ---------- |
| SSLv3   | 사용 금지 | POODLE 취약점 |
| TLS 1.0 | 사용 금지 | 취약점 존재     |
| TLS 1.1 | 사용 금지 | 취약점 존재     |
| TLS 1.2 | 허용    | 권장         |
| TLS 1.3 | 허용    | 최신, 가장 안전  |

***

## API 인증 <a href="#api-authentication" id="api-authentication"></a>

PlantPulse REST API는 두 가지 인증 방식을 지원합니다.

### ID/Password 인증 <a href="#api-auth-basic" id="api-auth-basic"></a>

매 요청마다 사용자 ID와 비밀번호를 전달하는 방식입니다.

```bash
# 로그인하여 세션 쿠키 획득
curl -X POST http://localhost/api/v3/login \
    -H "Content-Type: application/json" \
    -d '{"userId": "admin", "password": "admin123!"}' \
    -c cookies.txt

# 세션 쿠키를 사용하여 API 호출
curl -X GET http://localhost/api/v3/assets \
    -b cookies.txt
```

### Token 인증 <a href="#api-auth-token" id="api-auth-token"></a>

API 토큰을 발급받아 사용하는 방식으로, 자동화 및 외부 시스템 연동에 적합합니다.

```bash
# 1. 토큰 발급
curl -X POST http://localhost/api/v3/auth/token \
    -H "Content-Type: application/json" \
    -d '{"userId": "admin", "password": "admin123!"}'
# 응답: {"token": "eyJhbGciOiJIUzI1NiJ9..."}

# 2. 토큰을 사용하여 API 호출
curl -X GET http://localhost/api/v3/assets \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."

# 3. 토큰 갱신
curl -X POST http://localhost/api/v3/auth/refresh \
    -H "Authorization: Bearer eyJhbGciOiJIUzI1NiJ9..."
```

> **안내:** Token 방식은 세션이 필요 없으므로 서버 간 통신이나 스크립트에서 사용하기 편리합니다. 토큰의 기본 유효 시간은 24시간이며, `application.properties`에서 변경할 수 있습니다.

***

## 비밀번호 보안 <a href="#password-security" id="password-security"></a>

### 비밀번호 해싱 <a href="#password-hashing" id="password-hashing"></a>

PlantPulse는 사용자 비밀번호를 SHA-256 해시로 저장합니다. 원본 비밀번호는 서버에 저장되지 않습니다.

### 비밀번호 정책 권장값 <a href="#password-policy" id="password-policy"></a>

| 항목       | 권장값        | 설명                      |
| -------- | ---------- | ----------------------- |
| 최소 길이    | 8자 이상      | 12자 이상 권장               |
| 복잡도      | 3종 이상 조합   | 영문 대/소문자, 숫자, 특수문자 중 3종 |
| 변경 주기    | 90일        | 분기별 변경                  |
| 이력 관리    | 최근 3개      | 이전 비밀번호 재사용 금지          |
| 연속 실패 잠금 | 5회         | 5회 연속 실패 시 계정 잠금        |
| 잠금 해제    | 30분 또는 관리자 | 자동 해제 또는 수동 해제          |

***

## HTTP 보안 헤더 <a href="#http-security-headers" id="http-security-headers"></a>

웹 브라우저의 보안 기능을 활성화하기 위해 HTTP 응답 헤더를 설정합니다.

### 권장 보안 헤더 <a href="#recommended-headers" id="recommended-headers"></a>

| 헤더                          | 값                                     | 설명                    |
| --------------------------- | ------------------------------------- | --------------------- |
| `X-XSS-Protection`          | `1; mode=block`                       | 브라우저 XSS 필터 활성화       |
| `X-Content-Type-Options`    | `nosniff`                             | MIME 타입 스니핑 방지        |
| `X-Frame-Options`           | `SAMEORIGIN`                          | 클릭재킹(Clickjacking) 방지 |
| `Content-Security-Policy`   | `default-src 'self'`                  | 콘텐츠 소스 제한             |
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | HTTPS 강제 (HSTS)       |
| `Referrer-Policy`           | `strict-origin-when-cross-origin`     | 리퍼러 정보 제한             |

### Nginx 설정 예시 <a href="#nginx-headers" id="nginx-headers"></a>

리버스 프록시로 Nginx를 사용하는 경우 다음과 같이 보안 헤더를 추가합니다.

```nginx
server {
    listen 443 ssl http2;
    server_name plantpulse.example.com;

    # SSL 인증서
    ssl_certificate     /etc/ssl/certs/plantpulse.crt;
    ssl_certificate_key /etc/ssl/private/plantpulse.key;
    ssl_protocols       TLSv1.2 TLSv1.3;
    ssl_ciphers         HIGH:!aNULL:!MD5;

    # 보안 헤더
    add_header X-XSS-Protection "1; mode=block" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://127.0.0.1:80;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

# HTTP → HTTPS 리다이렉트
server {
    listen 80;
    server_name plantpulse.example.com;
    return 301 https://$host$request_uri;
}
```

***

## HTTP 메소드 제한 <a href="#http-method-restriction" id="http-method-restriction"></a>

불필요한 HTTP 메소드를 차단하여 정보 노출을 방지합니다.

```xml
<!-- plantpulse-server/app/plantpulse-server-web/WEB-INF/web.xml -->
<security-constraint>
    <web-resource-collection>
        <web-resource-name>RestrictMethods</web-resource-name>
        <url-pattern>/*</url-pattern>
        <http-method>OPTIONS</http-method>
        <http-method>TRACE</http-method>
        <http-method>HEAD</http-method>
    </web-resource-collection>
    <auth-constraint/>
</security-constraint>
```

> **안내:** OPTIONS 메소드는 CORS 프리플라이트 요청에 사용됩니다. 외부 시스템과 API 연동이 필요한 경우 OPTIONS 메소드를 허용해야 할 수 있습니다.

***

## 세션 보안 <a href="#session-security" id="session-security"></a>

### 쿠키 보안 설정 <a href="#cookie-security" id="cookie-security"></a>

```xml
<!-- plantpulse-server/app/plantpulse-server-web/WEB-INF/web.xml -->
<session-config>
    <session-timeout>30</session-timeout>
    <cookie-config>
        <http-only>true</http-only>    <!-- JavaScript에서 쿠키 접근 차단 -->
        <secure>true</secure>           <!-- HTTPS에서만 쿠키 전송 -->
        <max-age>1800</max-age>         <!-- 쿠키 유효 시간 (초) -->
    </cookie-config>
    <tracking-mode>COOKIE</tracking-mode>
</session-config>
```

| 설정              | 값        | 설명                                |
| --------------- | -------- | --------------------------------- |
| `http-only`     | `true`   | XSS를 통한 세션 탈취 방지                  |
| `secure`        | `true`   | HTTP에서 쿠키가 전송되지 않도록 방지 (HTTPS 필수) |
| `tracking-mode` | `COOKIE` | URL에 세션 ID가 노출되지 않도록 방지           |

### Session Fixation 방지 <a href="#session-fixation" id="session-fixation"></a>

PlantPulse는 로그인 성공 시 세션 ID를 재생성하여 Session Fixation 공격을 방지합니다. 이 기능은 `SecurityFilter`에 내장되어 있으며, 별도 설정 없이 자동으로 적용됩니다.

***

## 데이터베이스 보안 <a href="#database-security" id="database-security"></a>

### PostgreSQL <a href="#postgresql-security" id="postgresql-security"></a>

`pg_hba.conf` 파일에서 접근 허용 범위를 설정합니다.

```conf
# /opt/kopens/plantpulse-platform/plantpulse-storage/db/postgresql/data/pg_hba.conf

# TYPE  DATABASE        USER            ADDRESS                 METHOD
# 로컬 접근 (Unix 소켓)
local   all             all                                     md5

# 로컬호스트 접근
host    all             all             127.0.0.1/32            md5
host    all             all             ::1/128                 md5

# PlantPulse 서버 접근 (특정 IP만 허용)
host    plantpulse      plantpulse      10.0.0.0/24             md5

# 그 외 접근 차단 (기본)
# host  all             all             0.0.0.0/0               reject
```

추가 보안 설정 (`postgresql.conf`):

```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 <a href="#cassandra-security" id="cassandra-security"></a>

```yaml
# /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) <a href="#redis-security" id="redis-security"></a>

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

***

## 네트워크 보안 <a href="#network-security" id="network-security"></a>

### 방화벽 설정 <a href="#firewall" id="firewall"></a>

PlantPulse에서 외부 접근이 필요한 포트만 방화벽에서 허용해 주세요.

```bash
# 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
```

### 네트워크 세그멘테이션 <a href="#network-segmentation" id="network-segmentation"></a>

산업 환경에서는 네트워크를 다음과 같이 분리하는 것을 권장합니다.

| 네트워크 영역  | 용도                  | 예시          |
| -------- | ------------------- | ----------- |
| OT 네트워크  | PLC, SCADA, 센서      | 10.0.1.0/24 |
| DMZ      | PlantPulse 웹서버, API | 10.0.2.0/24 |
| IT 네트워크  | 사용자 접근, 관리          | 10.0.3.0/24 |
| 데이터 네트워크 | DB, 메시징             | 10.0.4.0/24 |

* OT 네트워크와 IT 네트워크는 직접 통신하지 않도록 해 주세요.
* PlantPulse 서버는 DMZ에 배치하여 양쪽 네트워크와 제한된 통신만 허용합니다.
* 데이터베이스는 데이터 네트워크에 배치하고, PlantPulse 서버에서만 접근을 허용합니다.

### VPN 접근 <a href="#vpn" id="vpn"></a>

원격에서 관리 콘솔에 접근해야 하는 경우 VPN을 통해 접속하도록 구성합니다.

* 관리 콘솔 및 SSH 접근은 반드시 VPN을 통해서만 허용해 주세요.
* VPN 계정은 개인별로 발급하고, 공유 계정을 사용하지 않도록 합니다.
* VPN 접속 로그를 기록하고 주기적으로 검토합니다.

***

## 보안 점검 체크리스트 <a href="#security-checklist" id="security-checklist"></a>

### 일일 점검 <a href="#daily-check" id="daily-check"></a>

| 점검 항목      | 확인 방법                |
| ---------- | -------------------- |
| 비정상 로그인 시도 | 감사 로그에서 로그인 실패 횟수 확인 |
| 시스템 에러 로그  | 보안 관련 에러/경고 로그 확인    |
| 서비스 가용성    | 헬스체크 API 응답 확인       |

### 주간 점검 <a href="#weekly-check" id="weekly-check"></a>

| 점검 항목        | 확인 방법             |
| ------------ | ----------------- |
| 사용자 계정 현황    | 미사용 계정, 비활성 계정 점검 |
| 권한 변경 이력     | 감사 로그에서 권한 변경 확인  |
| 방화벽 로그       | 차단된 접근 시도 확인      |
| SSL 인증서 유효기간 | 인증서 만료일 확인        |

### 월간 점검 <a href="#monthly-check" id="monthly-check"></a>

| 점검 항목      | 확인 방법                      |
| ---------- | -------------------------- |
| OS 보안 패치   | 최신 보안 업데이트 적용 확인           |
| 비밀번호 정책 준수 | 장기 미변경 비밀번호 확인             |
| DB 접근 권한   | pg\_hba.conf 등 접근 제어 설정 점검 |
| 백업 데이터 보안  | 백업 파일 암호화 및 접근 권한 확인       |
| 포트 스캔      | 불필요하게 열린 포트 확인             |
| TLS 설정     | 취약한 프로토콜/암호화 스위트 점검        |

***

## 보안 사고 대응 <a href="#incident-response" id="incident-response"></a>

보안 사고가 발생했거나 의심되는 경우 다음 절차에 따라 대응해 주세요.

### 1단계: 탐지 및 격리

```bash
# 의심 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/v3/sessions \
    -H "Authorization: Bearer <admin-token>"
```

### 2단계: 분석

```bash
# 로그인 이력 조회
grep "LOGIN\|LOGOUT\|AUTH" /opt/kopens/plantpulse-platform/plantpulse-server/app/plantpulse-server-web/logs/plantpulse.log

# 접근 로그 분석
grep "의심IP" /opt/kopens/plantpulse-platform/plantpulse-server/app/plantpulse-server-web/logs/access.log

# DB 접근 로그 (PostgreSQL)
grep "의심IP" /opt/kopens/plantpulse-platform/plantpulse-storage/db/postgresql/data/log/postgresql-*.log
```

### 3단계: 복구

* 침해된 계정의 비밀번호를 변경합니다.
* 영향 받은 시스템의 무결성을 확인합니다.
* 필요 시 백업에서 데이터를 복원합니다.

### 4단계: 사후 조치

* 사고 보고서를 작성합니다.
* 재발 방지 대책을 수립합니다.
* 보안 설정을 강화합니다.
* 관련 인원에게 보안 교육을 실시합니다.

> **연락처:** 보안 관련 문의 사항은 <webmaster@kopens.com> 으로 연락해 주세요.
