Compare commits
4 Commits
5fad740b7f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e6699912a5 | |||
| 979c9da6c0 | |||
| 9e0f908f19 | |||
| 5770f78c99 |
@@ -59,28 +59,30 @@ help:
|
|||||||
# ── Cluster lifecycle ─────────────────────────────────────────────────────────
|
# ── Cluster lifecycle ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
deploy:
|
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 -
|
@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 add strimzi https://strimzi.io/charts/ 2>/dev/null || true
|
||||||
@helm repo update strimzi 2>/dev/null
|
@helm repo update strimzi 2>/dev/null
|
||||||
@helm upgrade --install strimzi-kafka-operator strimzi/strimzi-kafka-operator \
|
@helm upgrade --install strimzi-kafka-operator strimzi/strimzi-kafka-operator \
|
||||||
--namespace $(NAMESPACE) \
|
--namespace $(NAMESPACE) \
|
||||||
--set watchAnyNamespace=false \
|
--set watchAnyNamespace=false \
|
||||||
--wait --timeout 5m
|
--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 \
|
@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; \
|
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"; \
|
echo " $$crd ready"; \
|
||||||
done
|
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 apply -f $(SCRIPT_DIR)kafka.yaml
|
||||||
@kubectl wait kafka/$(KAFKA_NAME) --for=condition=Ready --timeout=10m -n $(NAMESPACE)
|
@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
|
@kubectl apply -f $(SCRIPT_DIR)kafka-users.yaml
|
||||||
@for user in kafka-admin kafka-client; do \
|
@for user in kafka-admin kafka-client; do \
|
||||||
kubectl wait kafkauser/$$user --for=condition=Ready --timeout=120s -n $(NAMESPACE); \
|
kubectl wait kafkauser/$$user --for=condition=Ready --timeout=120s -n $(NAMESPACE); \
|
||||||
done
|
done
|
||||||
|
@echo "==> [6/6] Deploying the credentials store"
|
||||||
|
@kubectl apply -f $(SCRIPT_DIR)credentials-access.yaml
|
||||||
@echo ""
|
@echo ""
|
||||||
@echo "Cluster ready. Bootstrap: $(BOOTSTRAP)"
|
@echo "Cluster ready. Bootstrap: $(BOOTSTRAP)"
|
||||||
|
|
||||||
@@ -112,6 +114,7 @@ upgrade:
|
|||||||
|
|
||||||
teardown:
|
teardown:
|
||||||
@echo "==> Deleting KafkaUsers and Kafka cluster"
|
@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-users.yaml --ignore-not-found
|
||||||
@kubectl delete -f $(SCRIPT_DIR)kafka.yaml --ignore-not-found
|
@kubectl delete -f $(SCRIPT_DIR)kafka.yaml --ignore-not-found
|
||||||
@echo "==> Waiting for pods to terminate"
|
@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-admin` | `kafka-admin` | Super user — unrestricted access |
|
||||||
| `kafka-client` | `kafka-client` | Application user — Read/Write all topics and groups |
|
| `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
|
These two are all this repo declares. **An application's account is not added
|
||||||
write-only and prefix-scoped. A producer that can also read every topic is a
|
here** — it belongs to the application, whose own repo says what it may read and
|
||||||
producer that exfiltrates every other domain's events if it is ever
|
write, so that adding or removing the application adds or removes its access.
|
||||||
compromised.
|
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:
|
Strimzi stores credentials in Kubernetes secrets. Each secret contains two keys:
|
||||||
- `password` — the SCRAM password
|
- `password` — the SCRAM password
|
||||||
- `sasl.jaas.config` — the ready-to-use JAAS config string
|
- `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
|
### TLS
|
||||||
|
|
||||||
Strimzi generates and manages its own internal CA. The cluster CA certificate is stored in:
|
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
|
3. Waits for the Strimzi CRDs to be fully established
|
||||||
4. Applies `kafka.yaml` (KafkaNodePool + Kafka CRs) and waits for `Ready`
|
4. Applies `kafka.yaml` (KafkaNodePool + Kafka CRs) and waits for `Ready`
|
||||||
5. Applies `kafka-users.yaml` (KafkaUser 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.
|
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
|
## 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
|
```bash
|
||||||
kubectl apply -f kafka-users.yaml
|
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:
|
operations:
|
||||||
- Describe
|
- Describe
|
||||||
- Write
|
- 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