# BunkerWeb Integration

> Status: Version-specific
> Documentation baseline: v4.03.2

This page defines the SysWarden side of the BunkerWeb integration and migration
fence. It does not claim that any plugin release has completed its external
end-to-end matrix. Use only a partner version whose release notes explicitly
name compatibility with this frozen contract.

This page preserves the historical partner contract and its explicitly stated
later additions. The version-specific v4.10.0 candidate introduces a separate
HA v2 runtime with mutual TLS and additional writer and recovery requirements.
This page does not qualify that successor or define a mixed-version cluster
migration. For candidate laboratory work, review the
[HA v2 operator prerequisites](https://github.com/duggytuxy/syswarden/blob/8fc22a6bae4adfcf62d80e90da6387f2aa6db1c7/docs/technical/HA_V2_OPERATOR_PREREQUISITES.md)
and [native qualification plan](https://github.com/duggytuxy/syswarden/blob/8fc22a6bae4adfcf62d80e90da6387f2aa6db1c7/docs/technical/HA_V2_NATIVE_QUALIFICATION.md).

## Integration boundary

The BunkerWeb scheduler can use SysWarden's authenticated HA API to:

- push temporary Layer 7 bans into Linux nftables;
- withdraw only provenance records owned by the same claimed source and
  observed peer scope;
- read bounded blocklist, whitelist, status and telemetry data;
- migrate its own durable historical static submissions to provenance-aware
  ownership without inferring ownership from effective peer state.

The plugin uses only the HTTPS HA API. It must not edit SysWarden files, invoke
the local CLI or access a local process socket. Source-owned temporary records
are sent directly to every declared peer; SysWarden nodes do not relay them
transitively.

The local TUI is unrelated to this integration. It opens no listening socket.
The current product contains no browser terminal or remote PTY service and owns
no TCP 62027 listener or firewall permission.

## Prerequisites

- A published SysWarden release explicitly compatible with the frozen v4.03.2
  integration contract on every participating node. Before the next compatible
  Patch is public, only its exact merged candidate may be used in release
  qualification; it is not a production integration release.
- A compatible BunkerWeb plugin candidate.
- Console or out-of-band recovery access.
- TCP 62026 reachability on a dedicated management network.
- A high-entropy bearer token delivered through a protected secret channel.
- Exact member endpoints and an operator-approved inbound peer scope.
- Authenticated distribution of every SysWarden public HA certificate or CA.
- One operator-provided inventory containing every cluster member and every
  external legacy writer that can still submit historical static state.
- A durable BunkerWeb record of the historical static addresses it submitted to
  each exact peer.

An omitted peer or writer invalidates the migration campaign. The integrator
must never manufacture membership from what one peer happens to report.

## Enable the SysWarden API

Configure the HA listener and partner extensions:

```toml
[integrations.ha]
enabled = true
peer_ips = ["172.30.0.0/29"]
peer_port = 62026
token = "replace-with-a-random-shared-secret"

[integrations.bunkerweb]
enabled = true
```

An exact IP can authorize inbound requests and serve as an outbound SysWarden
peer. A CIDR authorizes inbound clients only and is never converted into an
outbound destination. The bearer token remains mandatory.

Validate and apply the reviewed configuration:

```console
sudo syswarden config validate
sudo syswarden reload
```

Configuration validation rejects HA when the token is empty, contains
whitespace or control characters, or the peer list is empty or invalid. A
rejected candidate never reaches reload mutation or service restart, so the
previously validated configuration and its running listener remain active.
Repair and validate the candidate before retrying the reload.

## TLS and authentication

Every `/ha/sync`, `/ha/status` and `/ha/telemetry` request requires TLS 1.3 and
`Authorization: Bearer`. HA cannot be activated from a configuration with an
empty or otherwise invalid bearer token.

The persistent server identity is stored at:

- `/var/lib/syswarden/ha/server.crt`
- `/var/lib/syswarden/ha/server.key`

Never copy `server.key` to a client. Transfer only the public certificate or CA
through a trusted channel and verify its SHA-256 fingerprint:

```console
openssl x509 -in /var/lib/syswarden/ha/server.crt -noout -fingerprint -sha256
```

Do not enable an insecure TLS bypass in production. A certificate identity
change invalidates the current manifest and resets every partner observation
window.

## Dialect selection

For one exact peer and one serialized cycle, issue one authenticated
`GET /ha/status`. Use the enriched dialect only when both `sync_ttl` and
`sync_provenance` are present. If either capability is missing, partial,
malformed or unavailable, stop the enriched handoff for that peer. Never relax
TLS or bearer authentication and never mix historical and enriched bodies in
one request.

The peer advertises `sync_ttl` and `sync_provenance` only while
`integrations.bunkerweb.enabled = true` is configured and the validated
configuration is active. Do not infer those capabilities from the candidate
version or from a previous status response.

Historical static submission uses the following template:

```json
{"ips":["REPLACE_WITH_CONTROLLED_PUBLIC_HOST"]}
```

An enriched temporary submission can use this template:

```json
{
  "bans": [
    {
      "ip": "REPLACE_WITH_CONTROLLED_PUBLIC_HOST",
      "ttl": 3600,
      "reason": "Layer 7 detection",
      "source": "bunkerweb"
    }
  ]
}
```

Replace each placeholder before creating the protected request body. Do not
submit the literal placeholder and do not substitute an administrative, local,
special-use, whitelisted or HA-peer address. Both historical and enriched POST
bodies accept canonical public host addresses only. CIDRs are rejected on POST.
Only an explicit historical `DELETE {"ips": [...]}` recovery request may name a
canonical CIDR that is already present in the historical ownership store.

The enriched contract accepts no more than 500 records per request. TTL is an
integer from 1 to 2,592,000 seconds. Host addresses must be canonical, `source`
is bounded approved ASCII and `reason` is bounded printable UTF-8.
Unknown fields, duplicate keys and mixed forms fail closed.

## Separate ownership stores

Historical static entries and provenance ledger entries are distinct stores:

- `POST {"ips": [...]}` creates historical static state;
- `DELETE {"ips": [...]}` explicitly removes historical static entries;
- `POST {"bans": [...]}` creates provenance-aware state;
- `DELETE {"bans": [...]}` removes provenance ledger entries only;
- deleting a provenance record never implicitly deletes a historical static
  entry;
- deleting one peer never cascades into another peer.

The enriched DELETE contract introduced by the historical v4.04.0 change set
was first published in the historical v4.04.2 release and is carried forward in
the version-specific v4.04.3 release. It returns an exact removal count after
successful processing:

```json
{"status":"ok","deleted":1}
```

`deleted` counts provenance claims actually removed for the exact canonical IP,
`source` and authenticated peer scope. It does not count distinct IP addresses
and does not assert that an address was removed from nftables. Another source,
peer or historical static claim can keep the effective ban active. A valid
source or scope mismatch is an idempotent no-op and returns
`{"status":"ok","deleted":0}`. Only a successful response carries this
completed-removal meaning; an error requires reconciliation before retry.

The BunkerWeb cleanup set comes only from its durable, peer-specific registry of
addresses it previously submitted historically. It must never infer ownership
from `GET /ha/sync`, provenance pagination, another peer or an effective union.
This prevents deletion of operator, WAAP or second-producer ownership.

A claim remains retained until one complete observation finds the address absent
from every declared peer and absent from BunkerWeb. An unavailable peer does not
count as absence. Membership changes reset every observation window.

## Operator activation manifest

One trusted operator control host creates one strict, canonical manifest from a
complete inventory. The manifest contains exact member endpoints, live TLS leaf
fingerprints, external legacy-writer identifiers, one random epoch,
`membership_sha256` and `legacy_writer_inventory_sha256`.

Create the operator inventory with this schema. The documentation address is an
example only and must be replaced with the canonical IP literal of the receiving
HA endpoint:

```json
{
  "schema_version": 1,
  "membership_scope": "one_receiving_api_endpoint_per_syswarden_node",
  "legacy_writer_ids": [
    "bunkerweb-primary"
  ],
  "members": [
    {
      "address": "192.0.2.10",
      "port": 62026
    }
  ]
}
```

The inventory path must be absolute and canonical. The input must be a regular,
root-owned mode `0600` file no larger than 1 MiB, reached through a protected
directory chain. JSON must be UTF-8 without a byte-order mark, duplicate or
unknown keys, or trailing data. `members` must contain at least one unique
address and port endpoint. Each address is a canonical IP literal without a
CIDR, zone, brackets or IPv4-mapped IPv6 form, and each port is from 1 through
65535. `legacy_writer_ids` is mandatory and may be `[]`; every non-empty value
must match `[a-z0-9][a-z0-9._-]{0,63}` and be unique. Manifest creation sorts
members and writer IDs canonically. `--assert-complete` is the operator's
explicit attestation that both inventories are complete.

```console
sudo syswarden ha-fence manifest create \
  --inventory /root/syswarden-ha-inventory.json \
  --output /root/syswarden-ha-manifest.json \
  --assert-complete
sudo syswarden ha-fence manifest verify \
  --manifest /root/syswarden-ha-manifest.json
sudo syswarden ha-fence engage \
  --manifest /root/syswarden-ha-manifest.json
```

The operator distributes that exact protected manifest to every member and to
the integrator. The integrator does not submit cluster membership or writer
state through another API. A member, endpoint, certificate or writer change
requires a new manifest.

## Dynamic fence proof

The capability `native_sync_fence_v1` announces schema support only. It is not
proof that the node is fenced or drained.

For every manifest member, send a unique 32-byte base64url challenge in
`X-SysWarden-HA-Challenge` and obtain one fresh authenticated
`GET /ha/status`. Accept proof only when all conditions hold in the same
response:

- `native_sync_fence.state` is `active_drained`;
- the returned challenge equals the request challenge;
- the live TLS leaf fingerprint equals the manifest member pin;
- `epoch` equals the manifest epoch;
- `membership_sha256` equals the manifest membership digest;
- `legacy_writer_inventory_sha256` equals the manifest writer digest;
- the `X-SysWarden-HA-Fence-Condition` response header equals the JSON
  `condition` value;
- server instance, generation and condition remain stable for the observation.

The integrator treats the epoch and both digest values as opaque,
case-sensitive strings. It compares them for exact equality with the
operator-provided manifest and does not recalculate either digest or
reimplement SysWarden canonicalization.

An unreachable member, partial view, invalid challenge echo, changed
certificate, changed server instance, changed generation, changed membership,
blind interval or reappearing address resets the campaign observation.

## Condition-bound cleanup

Every `DELETE {"ips": [...]}` migration request carries the exact observed
condition:

```text
X-SysWarden-HA-Fence-Condition: <condition>
```

While the fence is active and drained:

The outcomes are fail-closed: a missing condition receives HTTP 428, a
malformed condition receives HTTP 400 and a stale condition receives HTTP 412.
Each rejection performs no mutation.

| Request condition | Result | Mutation |
|---|---|---|
| Missing | HTTP 428 | None |
| Malformed | HTTP 400 | None |
| Stale or changed | HTTP 412 | None |
| Exact current condition | Normal authenticated request processing | Peer-scoped only |

HTTP 412 means the fence moved. Stop the cleanup, obtain a new all-member proof
and require an operator decision. Do not retry blindly.

A one-hour continuous absence window is additional evidence only. It is never
proof of drain and never permits claim release from an incomplete view. The
window must cover every declared peer, and a reappearance resets the timer and
must be logged.

## Fence release

Release requires the unchanged activation manifest and durable terminal closure
evidence for every external legacy writer:

```json
{
  "schema_version": 1,
  "epoch": "COPY_EXACT_EPOCH_FROM_MANIFEST",
  "membership_sha256": "COPY_EXACT_MEMBERSHIP_SHA256_FROM_MANIFEST",
  "legacy_writer_inventory_sha256": "COPY_EXACT_WRITER_SHA256_FROM_MANIFEST",
  "legacy_retry_queue_drained": true,
  "writers": [
    {
      "id": "bunkerweb-primary",
      "disposition": "migrated_enriched_only",
      "closure_generation": "REPLACE_WITH_DURABLE_GENERATION",
      "closed_at": "REPLACE_WITH_CANONICAL_UTC_RFC3339",
      "evidence_sha256": "REPLACE_WITH_LOWERCASE_EVIDENCE_SHA256"
    }
  ]
}
```

Replace every placeholder with evidence bound to the unchanged manifest. The
closure uses the same protected-file rules as the inventory and must match the
canonical indented JSON layout shown, including one final newline. `writers`
must have exactly the manifest writer IDs in canonical order. An empty manifest
writer inventory therefore requires `"writers": []`. The retry queue flag must
be `true`. Accepted terminal dispositions are `migrated_enriched_only`,
`disabled`, `credential_revoked` and `network_quarantined`.
`closure_generation` is 1 through 256 non-space printable ASCII characters,
`closed_at` is canonical whole-second UTC RFC3339, and `evidence_sha256` is one
lowercase SHA-256 digest of the durable closure evidence.

```console
sudo syswarden ha-fence release \
  --manifest /root/syswarden-ha-manifest.json \
  --writer-closure /root/syswarden-ha-writer-closure.json
```

No queued historical write may become eligible after release. Use
`ha-fence recover` with the same manifest after an interrupted engagement.
Never reuse a retired epoch or reconstruct a manifest from peer output.

## Partner settings

The compatible plugin documentation is authoritative for final names. The
current design uses these settings:

| Setting | Purpose |
|---|---|
| `SYSWARDEN_PEERS` | Complete SysWarden peer list |
| `SYSWARDEN_API_TOKEN` | Bearer token delivered as a secret |
| `SYSWARDEN_CA_BUNDLE` | Trusted CA or certificate bundle |
| `SYSWARDEN_SSL_FINGERPRINT` | Explicit SHA-256 leaf pin |
| `SYSWARDEN_ENFORCEMENT` | Partner audit or enforcing mode |
| `SYSWARDEN_BAN_CHUNK_SIZE` | Request batch size, never above 500 |
| `SYSWARDEN_BAN_MAX_ITEMS` | Per-pass safety limit |
| `SYSWARDEN_BAN_SCOPE_FILTER` | BunkerWeb services whose bans are propagated |
| `SYSWARDEN_BAN_MIN_TTL` | Minimum TTL eligible for propagation |
| `USE_SYSWARDEN_BLOCKLIST` | Enable downloaded blocklist use |
| `USE_SYSWARDEN_WHITELIST` | Enable downloaded whitelist use |

Start in audit mode, inspect scheduler requests and live status responses, then
enable only approved directions.

## Verification and release gate

An isolated end-to-end campaign must verify:

1. unauthenticated and untrusted TLS requests fail;
2. dialect selection uses exactly one status request per peer cycle;
3. temporary provenance expiry and owner-scoped withdrawal converge;
4. operator and second-source entries survive partner cycles;
5. historical cleanup comes only from the durable per-peer partner registry;
6. a peer outage freezes progress and membership change resets observations;
7. challenge, condition, epoch and opaque digest comparisons fail closed;
8. HTTP 428, 400 and 412 perform no mutation;
9. a resurrection is detected and deleted again under operator control;
10. fence release requires closure for every declared writer.

The historical v4.03.2 freeze required written partner confirmation against
the exact frozen contract. Any compatible plugin publication remains blocked
if there is unexplained partner-attributable static residue, lost ownership,
partial membership or missing writer closure.

Return to the [deployment tutorial](https://syswarden.io/docs/deployment-reference/) for package and
operator procedures.
