", "partition": , "timestamp": ""}
```
The sentinel marks the exact boundary between "migrated data" and "nothing else."
Tail continues until every sentinel is replicated to the target. At this point, the target has all source data up to and including the sentinel.
All consumer group committed offsets are fetched from the source cluster.
For each consumer group's committed offset on each partition:
```
target_offset = target_first + (source_committed - source_first)
```
Where `source_first`/`target_first` come from the offset map sidecar.
**Edge cases:**
- `source_committed < source_first` → **ResetToStart**: consumer was behind the earliest retained offset. Translated to `target_first`.
- `source_committed > source_last` → **Clamped**: consumer was ahead of the migration boundary. Translated to `target_last`.
- `target_first` missing → **Skip**: partition has no data on target. Logged as warning.
Translated offsets are committed to the target cluster's consumer group coordinators via the Kafka `OffsetCommit` API.
Before the tool logs `READY_FOR_CLIENT_SWITCH`, it fetches target earliest and latest offsets for every migrated partition. It blocks the client switch if the target log-start has advanced past the first copied offset or if the target end offset is behind the expected copied range. This catches retention or `DeleteRecords` truncation that a latest-offset-only drain check would miss.
This is the core differentiator. After cutover:
```
Source (ZK cluster): Target (KRaft cluster):
+------------------------------+ +------------------------------+
| Partition 0: | | Partition 0: |
| [0] msg-A | | [0] msg-A |
| [1] msg-B | | [1] msg-B |
| [2] msg-C <- committed | | [2] msg-C <- committed |
| [3] msg-D <- next msg | | [3] msg-D <- next msg |
| [4] msg-E | | [4] msg-E |
| [5] sentinel | | [5] sentinel |
+------------------------------+ +------------------------------+
```
The consumer group's committed offset points to the same **message content** on both clusters. When the consumer reconnects to the target, it reads `msg-D` next — exactly where it left off on the source.
The offset **numbers** may differ between source and target (due to compaction or replication timing), but the offset map ensures the translation is correct.
All ACL bindings are fetched from the source via the `DescribeAcls` API.
MSK internal bindings are filtered:
- `User:ANONYMOUS` (MSK internal)
- Bindings on `__consumer_offsets`, `__transaction_state`, and other internal topics
- `kafka-cluster:ClusterAction` on cluster resource (MSK auto-manages these)
| Policy | Source-only ACLs | Target-only ACLs |
| --- | --- | --- |
| `merge` | Created on target | Left in place |
| `replace` | Created on target | Reported (not deleted) |
| `refuse` | Error — migration stops | Error — migration stops |
When the target uses IAM auth, Kafka ACLs don't apply. Instead, the tool generates `access-map.json` — a mapping of each principal's permissions to the equivalent IAM actions. The operator applies these via their IAM tooling.
The evidence bundle is a JSON document signed with Ed25519:
```
{
"bundle_json": "",
"signature_b64": "",
"public_key_b64": ""
}
```
The `bundle_json` payload contains:
| Section | Contents |
| --- | --- |
| `migration_id` | Unique migration identifier |
| `tool_version` | kafka-backup version |
| `signed_at` | UTC timestamp |
| `config_fingerprint` | Hash of the migration config |
| `journal` | Complete state transition history |
| `source` / `target` | Cluster metadata snapshots |
| `plan` | Full migration plan |
| `topology` | Topics created/updated, configs applied |
| `acls` | ACL bindings copied, internals filtered |
| `seed` | Records, bytes, partitions transferred |
| `tail` | Records replayed during tail phase |
| `drain_final` | Per-partition lag at finalize time |
| `cutover` | Sentinel positions, freeze timing, offset translations |
| `validation` | All 5 check outcomes with per-partition detail; `counts_and_offsets.data.offset_floor_violations` records target offset-floor safety |
Evidence is uploaded to two keys:
1. Attempt-scoped immutable key: `s3://///evidence-attempts/--.json`
2. Latest alias for the migration: `s3://///evidence.json`
Each upload first tries S3 PutObject with COMPLIANCE-mode Object Lock retention derived from `evidence.retention`. If the bucket lacks Object Lock, the tool uploads without retention and logs a warning.
Every state transition is appended to `journal.jsonl`:
```
{"migration_id":"...","from":"seed","to":"tail","at":"2026-04-24T10:40:00Z","reason":"seed complete: 900 records, 4 partitions"}
{"migration_id":"...","from":"tail","to":"drain_ready","at":"2026-04-24T10:41:00Z","reason":"all partitions within lag tolerance"}
```
On resume, the journal is loaded from S3 (or local directory), the tip state is determined, and execution re-enters from the appropriate phase.
If the tip state is `failed`, the system walks backward through the journal to find the last non-failed state (capped at 4 hops to prevent oscillation). It then re-enters from that state.
Available from: `planned`, `precheck`, `topology_copy`, `seed`, `tail`, `drain_ready`
Rollback:
1. Verifies the resume fingerprint
2. Best-effort producer unfreeze (if frozen)
3. Uploads `rollback-report.json` to evidence bucket
4. Appends `→ rolled_back` to journal
5. Does **not** delete topics/data on target (manual cleanup)
- [MSK KRaft Migration Overview](https://kafkabackup.com/enterprise/msk-kraft-migration.md) — feature summary and quick start
- [Configuration Reference](https://kafkabackup.com/enterprise/msk-kraft-config-reference.md) — every config field
- [Production Runbook](https://kafkabackup.com/guides/msk-kraft-migration-runbook.md) — step-by-step guide