POC详情: c85494101ef9ca943892e07880e54555de6c33c6

来源
关联漏洞
标题: Apache Kafka Client 安全漏洞 (CVE-2025-27817)
描述:Apache Kafka Client是美国阿帕奇(Apache)基金会的一款Kafka客户端。 Apache Kafka Client存在安全漏洞,该漏洞源于配置数据验证不足,可能导致任意文件读取和服务端请求伪造。
描述
Apache Kafka 4.1.0 (KRaft) with Keycloak OAuth2 authentication using Strimzi - bypasses CVE-2025-27817 URL allowlist restriction
介绍
# Apache Kafka 4.1.0 with Keycloak OAuth2 Authentication

Production-ready Apache Kafka 4.1.0 (KRaft mode) with Keycloak 26.1.1 OAuth2/OIDC authentication using Strimzi Kafka image.

## Why This Project vs [kafka-oauth-keycloak-tls-demo](https://github.com/oriolrius/kafka-oauth-keycloak-tls-demo)

This is an **evolution** of the previous POC with significant improvements:

- **Strimzi OAuth 0.17.0** (vs 1.0.0) - stable production version bundled in Strimzi Kafka 0.48.0 image
- **No custom Docker build required** - uses official Strimzi image with OAuth pre-installed, eliminates Dockerfile complexity
- **CVE-2025-27817 awareness** - documents URL allowlist restriction and why Strimzi OAuth bypasses it
- **Simplified architecture** - single KRaft combined mode (broker+controller), not split architecture
- **librdkafka client focus** - tested with confluent-kafka-python (works without URL allowlist issues), not Java native clients
- **Comprehensive technical documentation** - production checklist, troubleshooting, performance tuning, principal mapping details
- **Cleaner certificate management** - included example certificates for immediate testing
- **Automated Keycloak setup** - scripted realm/client/mapper creation with audience configuration
- **Working Python test suite** - validates OAuth end-to-end message delivery
- **Explicit issuer URL handling** - documents internal vs external URL duality for token endpoint vs issuer validation

## Architecture

- **Kafka Distribution**: Strimzi Kafka image 0.48.0 (includes Apache Kafka 4.1.0 + Strimzi OAuth 0.17.0 pre-bundled)
- **Kafka Version**: Apache Kafka 4.1.0 (KRaft combined broker+controller)
- **OAuth Library**: Strimzi Kafka OAuth 0.17.0 (bundled in image, bypasses CVE-2025-27817 URL allowlist restriction)
- **OAuth Provider**: Keycloak 26.1.1
- **Security**: SASL_SSL (OAuth) for external clients, PLAINTEXT for inter-broker, SSL with self-signed CA

## CVE-2025-27817 Context

Apache Kafka 4.0.0+ introduced URL allowlist (`org.apache.kafka.sasl.oauthbearer.allowed.urls`) as JVM system property to fix SSRF/arbitrary file read vulnerability. This breaks standard OAuth usage in native Apache Kafka clients.

**Solution**: Strimzi Kafka OAuth library doesn't implement this restriction, enabling OAuth functionality with Kafka 4.1.0.

## Prerequisites

- Docker Compose
- Python 3.x with uv (for testing)
- OpenSSL (for certificate generation)

## Quick Start

```bash
# Generate SSL certificates
cd kafka-security
./generate-certs.sh
cd ..

# Start services
docker compose up -d

# Verify Keycloak
curl http://localhost:8080/health/ready

# Setup Keycloak realm and clients
./scripts/setup-keycloak.sh

# Test OAuth producer
source ~/.venv/bin/activate
uv pip install confluent-kafka
python tests/quick_test.py
```

## Network Topology

```
keycloak:8080 (HTTP) ←→ kafka-broker:9093 (SASL_SSL/OAuth)
                      ↔ kafka-broker:19092 (PLAINTEXT/inter-broker)
                      ↔ kafka-broker:29093 (PLAINTEXT/KRaft controller)
```

## SSL Configuration

### CA Structure
- **Root CA**: `kafka-security/ca-cert` + `ca-key`
- **Broker Keystore**: `kafka-security/broker/kafka.server.keystore.jks` (contains server cert + private key)
- **Broker Truststore**: `kafka-security/broker/kafka.server.truststore.jks` (contains CA cert)
- **Password**: `changeit` (all keystores/truststores)

### Certificate Details
```bash
# Broker certificate
CN=kafka-broker
SAN=DNS:kafka-broker,DNS:localhost,IP:127.0.0.1

# Validity: 3650 days
# Key algorithm: RSA 2048-bit
# Signature algorithm: SHA256withRSA
```

## Keycloak OAuth Configuration

### Realm: kafka-realm

#### Clients

**kafka-broker** (confidential)
- Client ID: `kafka-broker`
- Client Secret: Auto-generated by `setup-keycloak.sh`
- Purpose: Broker inter-broker OAuth authentication
- Mappers:
  - Audience mapper: adds `kafka-broker` to JWT `aud` claim
  - Username mapper: includes `preferred_username` in token

**kafka-producer** (confidential)
- Client ID: `kafka-producer`
- Client Secret: Auto-generated
- Purpose: External producer clients
- Grant: `client_credentials`
- Mappers: Same as kafka-broker

**kafka-consumer** (confidential)
- Client ID: `kafka-consumer`
- Client Secret: Auto-generated
- Purpose: External consumer clients
- Grant: `client_credentials`
- Mappers: Same as kafka-broker

### Token Endpoint
```
POST http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials
&client_id=kafka-producer
&client_secret=<secret>
&scope=profile email
```

### JWT Token Structure
```json
{
  "aud": ["kafka-broker", "account"],
  "iss": "http://localhost:8080/realms/kafka-realm",
  "azp": "kafka-producer",
  "preferred_username": "service-account-kafka-producer",
  "scope": "profile email"
}
```

## Kafka Configuration

### KRaft Mode (kraft-config.properties)

```properties
# Node identity
node.id=1
process.roles=broker,controller
controller.quorum.voters=1@kafka-broker:29093

# Listeners
listeners=SASL_SSL://0.0.0.0:9093,PLAINTEXT://0.0.0.0:19092,CONTROLLER://0.0.0.0:29093
advertised.listeners=SASL_SSL://localhost:9093,PLAINTEXT://kafka-broker:19092
listener.security.protocol.map=SASL_SSL:SASL_SSL,PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT
inter.broker.listener.name=PLAINTEXT
controller.listener.names=CONTROLLER

# SASL mechanism
sasl.enabled.mechanisms=OAUTHBEARER

# Strimzi OAuth handlers (per-listener for SASL_SSL)
listener.name.sasl_ssl.oauthbearer.sasl.login.callback.handler.class=io.strimzi.kafka.oauth.client.JaasClientOauthLoginCallbackHandler
listener.name.sasl_ssl.oauthbearer.sasl.server.callback.handler.class=io.strimzi.kafka.oauth.server.JaasServerOauthValidatorCallbackHandler

# OAuth configuration via JAAS
listener.name.sasl_ssl.oauthbearer.sasl.jaas.config=org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule required \
  oauth.client.id="kafka-broker" \
  oauth.client.secret="<secret>" \
  oauth.token.endpoint.uri="http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/token" \
  oauth.valid.issuer.uri="http://localhost:8080/realms/kafka-realm" \
  oauth.jwks.endpoint.uri="http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/certs" \
  oauth.username.claim="preferred_username";
```

### Key Strimzi OAuth Parameters

- `oauth.client.id`: Client identifier for token acquisition
- `oauth.client.secret`: Client secret for token acquisition
- `oauth.token.endpoint.uri`: Keycloak token endpoint (broker uses internal hostname `keycloak:8080`)
- `oauth.valid.issuer.uri`: Expected JWT issuer (must match token `iss` claim, uses external `localhost:8080`)
- `oauth.jwks.endpoint.uri`: JWKS endpoint for JWT signature validation
- `oauth.username.claim`: JWT claim for principal extraction

### Authorization

```properties
authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer
super.users=User:kafka-broker;User:ANONYMOUS
allow.everyone.if.no.acl.found=true
```

**Note**: Currently permissive for testing. Production should use ACLs.

## Client Configuration

### Python Producer (confluent-kafka)

```python
from confluent_kafka import Producer

conf = {
    'bootstrap.servers': 'localhost:9093',
    'security.protocol': 'SASL_SSL',
    'sasl.mechanisms': 'OAUTHBEARER',
    'sasl.oauthbearer.method': 'oidc',
    'sasl.oauthbearer.client.id': 'kafka-producer',
    'sasl.oauthbearer.client.secret': '<secret>',
    'sasl.oauthbearer.token.endpoint.url': 'http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token',
    'ssl.ca.location': 'kafka-security/ca-cert',
    'ssl.endpoint.identification.algorithm': 'none',
}

producer = Producer(conf)
producer.produce('topic', b'message')
producer.flush()
```

### Python Consumer (confluent-kafka)

```python
from confluent_kafka import Consumer

conf = {
    'bootstrap.servers': 'localhost:9093',
    'group.id': 'test-group',
    'security.protocol': 'SASL_SSL',
    'sasl.mechanisms': 'OAUTHBEARER',
    'sasl.oauthbearer.method': 'oidc',
    'sasl.oauthbearer.client.id': 'kafka-consumer',
    'sasl.oauthbearer.client.secret': '<secret>',
    'sasl.oauthbearer.token.endpoint.url': 'http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token',
    'ssl.ca.location': 'kafka-security/ca-cert',
    'ssl.endpoint.identification.algorithm': 'none',
    'auto.offset.reset': 'earliest',
}

consumer = Consumer(conf)
consumer.subscribe(['topic'])
while True:
    msg = consumer.poll(1.0)
    if msg: print(msg.value())
```

### Why librdkafka Works

confluent-kafka-python uses librdkafka (C library) which implements OAuth via `sasl.oauthbearer.method=oidc`. This implementation doesn't check the `org.apache.kafka.sasl.oauthbearer.allowed.urls` system property that blocks native Apache Kafka Java clients.

## Troubleshooting

### Verify OAuth Token

```bash
TOKEN=$(curl -s -X POST http://localhost:8080/realms/kafka-realm/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=kafka-producer" \
  -d "client_secret=<secret>" | jq -r .access_token)

echo $TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | jq .
```

Expected claims:
```json
{
  "aud": ["kafka-broker", "account"],
  "iss": "http://localhost:8080/realms/kafka-realm",
  "azp": "kafka-producer",
  "preferred_username": "service-account-kafka-producer"
}
```

### Check Broker OAuth Logs

```bash
docker logs kafka-broker 2>&1 | grep -E "Strimzi|JWTSignatureValidator|OAUTHBEARER"
```

Expected:
```
[io.strimzi.kafka.oauth.validator.JWTSignatureValidator] JWKS keys change detected
```

### Verify Broker Listeners

```bash
docker exec kafka-broker netstat -tlnp | grep java
```

Expected:
```
tcp6  0.0.0.0:9093   LISTEN  (SASL_SSL)
tcp6  0.0.0.0:19092  LISTEN  (PLAINTEXT)
tcp6  0.0.0.0:29093  LISTEN  (CONTROLLER)
```

### Check KRaft Metadata

```bash
docker exec kafka-broker cat /var/lib/kafka/data/meta.properties
```

Expected:
```
version=1
cluster.id=kafka-cluster-01
node.id=1
```

### Common Issues

**Issue**: `{"status":"invalid_token"}`
- **Cause**: JWT signature validation failure
- **Fix**: Verify `oauth.jwks.endpoint.uri` is reachable from broker container
- **Check**: `docker exec kafka-broker curl http://keycloak:8080/realms/kafka-realm/protocol/openid-connect/certs`

**Issue**: `Token audience mismatch`
- **Cause**: JWT `aud` claim doesn't contain `kafka-broker`
- **Fix**: Run `./scripts/setup-keycloak.sh` to add audience mapper
- **Verify**: Decode token and check `aud` claim includes `kafka-broker`

**Issue**: `Token issuer mismatch`
- **Cause**: JWT `iss` doesn't match `oauth.valid.issuer.uri`
- **Fix**: Ensure `oauth.valid.issuer.uri=http://localhost:8080/realms/kafka-realm` (external hostname)
- **Note**: Broker uses `http://keycloak:8080` for token endpoint but validates against `http://localhost:8080` issuer

**Issue**: Native Java Kafka clients fail with URL allowlist error
- **Cause**: CVE-2025-27817 fix in Apache Kafka 4.1.0
- **Fix**: Use librdkafka-based clients (confluent-kafka-python) or Strimzi OAuth on broker side (already configured)

## Performance Tuning

### Token Refresh

JWT tokens from Keycloak have 5-minute expiry. Strimzi OAuth automatically handles refresh:
- `oauth.refresh.token`: Not used (client_credentials grant)
- Token cached and refreshed 30s before expiry

### JWKS Caching

```properties
sasl.oauthbearer.jwks.endpoint.refresh.ms=3600000  # 1 hour
sasl.oauthbearer.jwks.endpoint.retry.backoff.ms=100
sasl.oauthbearer.jwks.endpoint.retry.backoff.max.ms=10000
```

### Connection Settings

```properties
connections.max.idle.ms=600000
connection.failed.authentication.delay.ms=1000
```

## Production Checklist

- [ ] Replace self-signed certificates with CA-signed certificates
- [ ] Update `ssl.endpoint.identification.algorithm=https` (remove `none`)
- [ ] Configure proper ACLs (remove `allow.everyone.if.no.acl.found=true`)
- [ ] Set up ACLs:
  ```bash
  kafka-acls --bootstrap-server localhost:9093 \
    --command-config admin.properties \
    --add --allow-principal User:kafka-producer \
    --operation Write --topic '*'
  ```
- [ ] Rotate Keycloak client secrets
- [ ] Enable Keycloak HTTPS
- [ ] Update `oauth.token.endpoint.uri` and `oauth.jwks.endpoint.uri` to HTTPS URLs
- [ ] Configure Kafka monitoring (JMX, Prometheus)
- [ ] Set up log aggregation for OAuth audit trail
- [ ] Test failover scenarios
- [ ] Document secret rotation procedures
- [ ] Enable Keycloak user federation (LDAP/AD) if needed

## Directory Structure

```
.
├── docker-compose.yml              # Orchestration
├── .env                            # Secrets (gitignored)
├── kafka-config/
│   ├── kraft-config.properties     # Kafka broker configuration
│   ├── producer.properties         # Producer OAuth config (for CLI tools)
│   └── consumer.properties         # Consumer OAuth config (for CLI tools)
├── kafka-security/
│   ├── generate-certs.sh           # SSL certificate generator
│   ├── ca-cert                     # Root CA certificate
│   ├── ca-key                      # Root CA private key
│   └── broker/
│       ├── kafka.server.keystore.jks
│       └── kafka.server.truststore.jks
├── scripts/
│   └── setup-keycloak.sh           # Keycloak realm/client setup
└── tests/
    └── quick_test.py               # OAuth validation test

```

## Technical Notes

### Why Strimzi Kafka Image Instead of Apache Kafka Official Image

The Strimzi Kafka image (`quay.io/strimzi/kafka:0.48.0-kafka-4.1.0`) is used instead of the official Apache Kafka image because:

1. **Bundled OAuth Support**: Includes Strimzi OAuth 0.17.0 library pre-installed (classes: `io.strimzi.kafka.oauth.*`)
2. **CVE-2025-27817 Bypass**: Strimzi OAuth library doesn't implement the URL allowlist restriction that breaks native Kafka OAuth
3. **Production Ready**: Battle-tested in Kubernetes environments via Strimzi Operator
4. **Single Image**: No need to manually download and mount OAuth JAR files

**Image breakdown**:
- Strimzi Kafka **0.48.0** = Docker image version/release
- Apache Kafka **4.1.0** = Kafka broker version bundled inside
- Strimzi OAuth **0.17.0** = OAuth library version bundled inside

### Issuer URL Duality

Broker configuration has two URLs:
- `oauth.token.endpoint.uri=http://keycloak:8080/...` (internal Docker network)
- `oauth.valid.issuer.uri=http://localhost:8080/...` (external, matches JWT `iss` claim)

This is because:
- Broker fetches tokens using internal DNS name
- Keycloak issues tokens with external issuer URL (configured in realm settings)
- JWT validation requires exact issuer match

### Principal Mapping

Broker extracts principal from JWT `preferred_username` claim:
```
service-account-kafka-producer → User:service-account-kafka-producer
```

ACLs reference this principal for authorization.

## Version Compatibility

| Component | Version | Notes |
|-----------|---------|-------|
| Apache Kafka | 4.1.0 | KRaft mode (no ZooKeeper) |
| Strimzi Kafka Image | 0.48.0 | Docker image: `quay.io/strimzi/kafka:0.48.0-kafka-4.1.0` |
| Strimzi OAuth Library | 0.17.0 | Pre-bundled in Strimzi Kafka 0.48.0 image |
| Keycloak | 26.1.1 | Latest LTS |
| librdkafka | 2.12.0+ | OIDC OAuth support |
| confluent-kafka-python | 2.12.0+ | Matches librdkafka version |

## References

- [Strimzi Kafka OAuth](https://github.com/strimzi/strimzi-kafka-oauth)
- [Apache Kafka Security](https://kafka.apache.org/documentation/#security)
- [Keycloak OIDC](https://www.keycloak.org/docs/latest/securing_apps/#_oidc)
- [CVE-2025-27817](https://nvd.nist.gov/vuln/detail/CVE-2025-27817)
- [KRaft Mode](https://kafka.apache.org/documentation/#kraft)
文件快照

[4.0K] /data/pocs/c85494101ef9ca943892e07880e54555de6c33c6 ├── [2.4K] docker-compose.yml ├── [4.0K] kafka-config │   ├── [ 713] admin.properties │   ├── [ 744] admin.properties.template │   ├── [ 715] consumer.properties │   ├── [ 751] consumer.properties.template │   ├── [2.3K] kraft-config.properties │   ├── [ 715] producer.properties │   └── [ 751] producer.properties.template ├── [4.0K] kafka-security │   ├── [4.0K] broker │   │   ├── [1.1K] kafka-broker.csr │   │   ├── [1.3K] kafka-broker-signed.pem │   │   ├── [ 99] kafka_server_jaas.conf │   │   ├── [4.8K] kafka.server.keystore.jks │   │   ├── [1.3K] kafka.server.truststore.jks │   │   ├── [ 921] oauth.properties │   │   └── [ 70] v3.ext │   ├── [4.0K] ca │   │   ├── [1.3K] ca-cert.pem │   │   └── [1.8K] ca-key.pem │   └── [4.0K] client │   ├── [1.1K] kafka-client.csr │   ├── [4.6K] kafka.client.keystore.jks │   ├── [1.2K] kafka-client-signed.pem │   └── [1.3K] kafka.client.truststore.jks ├── [ 15K] README.md ├── [4.0K] scripts │   ├── [1.9K] fix-audience.sh │   ├── [4.4K] generate-certs.sh │   ├── [2.1K] init-kafka.sh │   └── [9.2K] setup-keycloak.sh └── [4.0K] tests ├── [1.6K] quick_test.py ├── [4.7K] test_consumer.py └── [7.6K] test_producer.py 7 directories, 29 files
神龙机器人已为您缓存
备注
    1. 建议优先通过来源进行访问。
    2. 如果因为来源失效或无法访问,请发送邮箱到 f.jinxu#gmail.com 索取本地快照(把 # 换成 @)。
    3. 神龙已为您对POC代码进行快照,为了长期维护,请考虑为本地POC付费,感谢您的支持。