Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6699912a5 | |||
| 979c9da6c0 | |||
| 9e0f908f19 | |||
| 5770f78c99 |
@@ -59,28 +59,30 @@ help:
|
||||
# ── Cluster lifecycle ─────────────────────────────────────────────────────────
|
||||
|
||||
deploy:
|
||||
@echo "==> [1/5] Creating namespace '$(NAMESPACE)'"
|
||||
@echo "==> [1/6] Creating namespace '$(NAMESPACE)'"
|
||||
@kubectl create namespace $(NAMESPACE) --dry-run=client -o yaml | kubectl apply -f -
|
||||
@echo "==> [2/5] Installing Strimzi operator"
|
||||
@echo "==> [2/6] Installing Strimzi operator"
|
||||
@helm repo add strimzi https://strimzi.io/charts/ 2>/dev/null || true
|
||||
@helm repo update strimzi 2>/dev/null
|
||||
@helm upgrade --install strimzi-kafka-operator strimzi/strimzi-kafka-operator \
|
||||
--namespace $(NAMESPACE) \
|
||||
--set watchAnyNamespace=false \
|
||||
--wait --timeout 5m
|
||||
@echo "==> [3/5] Waiting for Strimzi CRDs"
|
||||
@echo "==> [3/6] Waiting for Strimzi CRDs"
|
||||
@for crd in kafkas.kafka.strimzi.io kafkanodepools.kafka.strimzi.io kafkausers.kafka.strimzi.io; do \
|
||||
until kubectl get crd "$$crd" -o jsonpath='{.status.conditions[?(@.type=="Established")].status}' 2>/dev/null | grep -q True; do sleep 2; done; \
|
||||
echo " $$crd ready"; \
|
||||
done
|
||||
@echo "==> [4/5] Deploying Kafka cluster (KRaft, SASL_SSL, SCRAM-SHA-512)"
|
||||
@echo "==> [4/6] Deploying Kafka cluster (KRaft, SASL_SSL, SCRAM-SHA-512)"
|
||||
@kubectl apply -f $(SCRIPT_DIR)kafka.yaml
|
||||
@kubectl wait kafka/$(KAFKA_NAME) --for=condition=Ready --timeout=10m -n $(NAMESPACE)
|
||||
@echo "==> [5/5] Deploying KafkaUsers"
|
||||
@echo "==> [5/6] Deploying KafkaUsers"
|
||||
@kubectl apply -f $(SCRIPT_DIR)kafka-users.yaml
|
||||
@for user in kafka-admin kafka-client; do \
|
||||
kubectl wait kafkauser/$$user --for=condition=Ready --timeout=120s -n $(NAMESPACE); \
|
||||
done
|
||||
@echo "==> [6/6] Deploying the credentials store"
|
||||
@kubectl apply -f $(SCRIPT_DIR)credentials-access.yaml
|
||||
@echo ""
|
||||
@echo "Cluster ready. Bootstrap: $(BOOTSTRAP)"
|
||||
|
||||
@@ -112,6 +114,7 @@ upgrade:
|
||||
|
||||
teardown:
|
||||
@echo "==> Deleting KafkaUsers and Kafka cluster"
|
||||
@kubectl delete -f $(SCRIPT_DIR)credentials-access.yaml --ignore-not-found
|
||||
@kubectl delete -f $(SCRIPT_DIR)kafka-users.yaml --ignore-not-found
|
||||
@kubectl delete -f $(SCRIPT_DIR)kafka.yaml --ignore-not-found
|
||||
@echo "==> Waiting for pods to terminate"
|
||||
|
||||
@@ -40,17 +40,71 @@ kafka-kafka-bootstrap.kafka.svc.cluster.local:9093
|
||||
|---|---|---|
|
||||
| `kafka-admin` | `kafka-admin` | Super user — unrestricted access |
|
||||
| `kafka-client` | `kafka-client` | Application user — Read/Write all topics and groups |
|
||||
| `classic-models-api` | `classic-models-api` | Producer for `classic-models.*` — **Write and Describe only**, no Read |
|
||||
|
||||
`classic-models-api` publishes domain events and consumes nothing, so it is
|
||||
write-only and prefix-scoped. A producer that can also read every topic is a
|
||||
producer that exfiltrates every other domain's events if it is ever
|
||||
compromised.
|
||||
These two are all this repo declares. **An application's account is not added
|
||||
here** — it belongs to the application, whose own repo says what it may read and
|
||||
write, so that adding or removing the application adds or removes its access.
|
||||
This repo deploys a cluster and holds no opinion about who uses it.
|
||||
|
||||
`kafka-users.yaml` carries the shape to copy. Grant **one direction, on one
|
||||
prefix**: a producer that can also read every topic exfiltrates every other
|
||||
application's data if it is ever compromised, and a consumer that can write can
|
||||
raise an event someone else acts on.
|
||||
|
||||
`kafka-client` is the wide-open template — useful for a smoke test, and for
|
||||
nothing that runs unattended.
|
||||
|
||||
Strimzi stores credentials in Kubernetes secrets. Each secret contains two keys:
|
||||
- `password` — the SCRAM password
|
||||
- `sasl.jaas.config` — the ready-to-use JAAS config string
|
||||
|
||||
### Topics
|
||||
|
||||
This repo declares none, for the same reason it declares no application account:
|
||||
a topic belongs to whoever publishes on it, and its partition count, retention
|
||||
and cleanup policy are that application's decisions, not the broker's.
|
||||
|
||||
Declare them as `KafkaTopic` CRs from the producer's own repo, **in this
|
||||
namespace** — Strimzi runs with `watchAnyNamespace=false`, so one created
|
||||
anywhere else is ignored, silently.
|
||||
|
||||
Declare them rather than relying on auto-creation: an application account has no
|
||||
`Create` ACL, so a producer would simply be denied, and an auto-created topic
|
||||
takes broker defaults nobody chose. Partition count is the ceiling on consumer
|
||||
parallelism and **cannot be lowered later**.
|
||||
|
||||
`make topic-create TOPIC=… PARTITIONS=…` exists for a scratch topic, not for
|
||||
anything an application depends on.
|
||||
|
||||
### Getting credentials into another namespace
|
||||
|
||||
A Secret cannot be mounted across namespaces, and Strimzi writes a KafkaUser's
|
||||
password into **this** one. `credentials-access.yaml` therefore declares a
|
||||
`ClusterSecretStore` named `kafka-credentials` (External Secrets, `kubernetes`
|
||||
provider) backed by a ServiceAccount that can read Secrets here and nowhere
|
||||
else. An application namespace then pulls its own password and the cluster CA
|
||||
with an `ExternalSecret`:
|
||||
|
||||
```yaml
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: kafka-credentials
|
||||
data:
|
||||
- secretKey: password
|
||||
remoteRef: {key: <kafkauser-name>, property: password}
|
||||
- secretKey: ca.crt
|
||||
remoteRef: {key: kafka-cluster-ca-cert, property: ca.crt}
|
||||
```
|
||||
|
||||
The password stays generated and rotatable by Strimzi; the copy follows. The
|
||||
alternative — copying it by hand — drifts the first time the user is
|
||||
regenerated.
|
||||
|
||||
**The trade, stated:** any namespace allowed to create an `ExternalSecret`
|
||||
against this store can pull **any** Kafka account's credentials. Acceptable with
|
||||
one operator; on a shared cluster, narrow the Role with `resourceNames` and run
|
||||
one store per application.
|
||||
|
||||
### TLS
|
||||
|
||||
Strimzi generates and manages its own internal CA. The cluster CA certificate is stored in:
|
||||
@@ -125,6 +179,7 @@ The target performs the following steps:
|
||||
3. Waits for the Strimzi CRDs to be fully established
|
||||
4. Applies `kafka.yaml` (KafkaNodePool + Kafka CRs) and waits for `Ready`
|
||||
5. Applies `kafka-users.yaml` (KafkaUser CRs) and waits for `Ready`
|
||||
6. Applies `credentials-access.yaml` (the cross-namespace credentials store)
|
||||
|
||||
Expected duration: **4–6 minutes** on a single-node k3s cluster.
|
||||
|
||||
@@ -223,7 +278,9 @@ The job is cleaned up automatically after 1 minute (`ttlSecondsAfterFinished: 60
|
||||
|
||||
## Adding a new user
|
||||
|
||||
Create a new `KafkaUser` manifest following the `kafka-client` pattern in `kafka-users.yaml`, then apply it:
|
||||
Copy the pattern documented at the bottom of `kafka-users.yaml` into the
|
||||
application's **own** repo, then apply it — into this namespace, since Strimzi
|
||||
watches no other:
|
||||
|
||||
```bash
|
||||
kubectl apply -f kafka-users.yaml
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
---
|
||||
# Letting an application namespace read its own Kafka credentials.
|
||||
#
|
||||
# Strimzi writes a KafkaUser's SCRAM password into a Secret in *this* namespace,
|
||||
# and a Secret cannot be mounted across namespaces. The alternatives are to copy
|
||||
# the password by hand (it then drifts the first time the user is regenerated)
|
||||
# or to put it in Vault (a second copy of a credential the cluster already
|
||||
# owns, kept in sync by nobody).
|
||||
#
|
||||
# Instead External Secrets reads it here and materialises it there. The password
|
||||
# stays generated by, and rotatable by, Strimzi; the copy follows.
|
||||
#
|
||||
# The ServiceAccount below can read Secrets in this namespace ONLY, which
|
||||
# includes every KafkaUser's password. That is the trade: any namespace allowed
|
||||
# to create an ExternalSecret against this store can pull any Kafka account's
|
||||
# credentials. Acceptable in a lab with one operator; in a shared cluster,
|
||||
# narrow the Role with resourceNames and run one store per application.
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: kafka-credentials-reader
|
||||
namespace: kafka
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: kafka-credentials-reader
|
||||
namespace: kafka
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: kafka-credentials-reader
|
||||
namespace: kafka
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: Role
|
||||
name: kafka-credentials-reader
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kafka-credentials-reader
|
||||
namespace: kafka
|
||||
---
|
||||
# External Secrets authenticates as that ServiceAccount rather than as itself,
|
||||
# so what the store can reach is bounded by the Role above and visible in RBAC
|
||||
# rather than in the operator's own permissions.
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ClusterSecretStore
|
||||
metadata:
|
||||
name: kafka-credentials
|
||||
spec:
|
||||
provider:
|
||||
kubernetes:
|
||||
remoteNamespace: kafka
|
||||
server:
|
||||
caProvider:
|
||||
type: ConfigMap
|
||||
name: kube-root-ca.crt
|
||||
key: ca.crt
|
||||
namespace: kafka
|
||||
auth:
|
||||
serviceAccount:
|
||||
name: kafka-credentials-reader
|
||||
namespace: kafka
|
||||
+35
-26
@@ -52,30 +52,39 @@ spec:
|
||||
operations:
|
||||
- Describe
|
||||
- Write
|
||||
---
|
||||
# The classic-models API, which publishes domain events and consumes nothing.
|
||||
#
|
||||
# Write-only and topic-scoped: a producer that can also read every topic is a
|
||||
# producer that can exfiltrate every other domain's events if it is ever
|
||||
# compromised. Describe is needed for metadata; it does not grant reads.
|
||||
apiVersion: kafka.strimzi.io/v1
|
||||
kind: KafkaUser
|
||||
metadata:
|
||||
name: classic-models-api
|
||||
namespace: kafka
|
||||
labels:
|
||||
strimzi.io/cluster: kafka
|
||||
spec:
|
||||
authentication:
|
||||
type: scram-sha-512
|
||||
authorization:
|
||||
type: simple
|
||||
acls:
|
||||
- resource:
|
||||
type: topic
|
||||
name: classic-models.
|
||||
patternType: prefix
|
||||
operations:
|
||||
- Write
|
||||
- Describe
|
||||
|
||||
# --- adding an application account ----------------------------------------
|
||||
#
|
||||
# Do not add one here. An account belongs to the application that uses it: its
|
||||
# own repo says what it may read and write, and adding or removing the
|
||||
# application adds or removes its access. This repo deploys a cluster and holds
|
||||
# no opinion about who uses it.
|
||||
#
|
||||
# Create the CR from the application's own repo, in **this** namespace — Strimzi
|
||||
# runs with `watchAnyNamespace=false`, so a KafkaUser anywhere else is ignored,
|
||||
# silently. Copy the shape below and cut it down to what the application does:
|
||||
#
|
||||
# apiVersion: kafka.strimzi.io/v1
|
||||
# kind: KafkaUser
|
||||
# metadata:
|
||||
# name: <application>
|
||||
# namespace: kafka
|
||||
# labels:
|
||||
# strimzi.io/cluster: kafka
|
||||
# spec:
|
||||
# authentication:
|
||||
# type: scram-sha-512
|
||||
# authorization:
|
||||
# type: simple
|
||||
# acls:
|
||||
# - resource: {type: topic, name: <prefix>, patternType: prefix}
|
||||
# operations: [Write, Describe] # a producer
|
||||
# - resource: {type: group, name: <prefix>, patternType: prefix}
|
||||
# operations: [Read, Describe] # a consumer's group
|
||||
#
|
||||
# A producer that can also read every topic exfiltrates every other
|
||||
# application's data if it is ever compromised, and a consumer that can write
|
||||
# can raise an event someone else acts on. Grant one direction, on one prefix.
|
||||
#
|
||||
# `kafka-client` above is the wide-open template: useful for a smoke test, and
|
||||
# for nothing that runs unattended.
|
||||
|
||||
Reference in New Issue
Block a user