
moselwal/crowdsec-bridge — CrowdSec LAPI inside the TYPO3 backend.
moselwal/crowdsec-bridge brings the operationally relevant subset of the CrowdSec Local API into a TYPO3 14 backend module: list decisions, ban and unban IPs, inspect alerts, check the LAPI heartbeat — all without container shell or SSH access. Read-only by default, write only for the dedicated operator role, every write attempt is recorded in sys_log for forensic traceability.
Six building blocks
- Decisions tab — paginated list of all active decisions (50 per page), sorted by creation time descending. Columns: ID, scope, type, value, remaining time, origin, scenario. Filters for scope (
Ip/Range/Country/As), type (ban/captcha) and origin. - IP quick-check — a binary banned / not-banned answer in under two seconds, including decision ID and remaining time. Server-side IP validation, every check is audit-logged.
- Add decision — operators create new decisions: scope, type, value, duration preset (1h / 24h / 7d / 30d / custom), reason (5–1000 chars). The server hardcodes
origin = "typo3"— user input cannot override it. - Delete decision — through a TYPO3-native confirmation modal. Foreign-origin decisions (created by CrowdSec or
cscli) are protected against accidental deletion; only admins with an explicit override may delete them. - Alerts tab — paginated read-only list of recent CrowdSec alerts (50 per page). Filters for source scope and scenario (case-insensitive substring). Detail view with the full source object (scope, value, IP, AS number, country, geo coordinates), labels and up to 50 linked decisions — directly bookmarkable.
- LAPI heartbeat banner — status, CrowdSec version, bouncer name and timestamp of the last check. Never cached.
Requirements
- TYPO3 14.3+
- PHP 8.5+
- CrowdSec 1.6+
psr/http-clientandpsr/http-factory1.0+- Optional: Redis or Valkey plus moselwal/keyvalue-store for the optional decision-list cache
- Extension key
crowdsec_bridge, namespaceMoselwal\CrowdSecBridge, MIT license
Architecture: strict DDD four-layer split
The layers are enforced via deptrac.yaml in CI. Upward imports are forbidden; every commit goes through the gate.
Classes/
├── Domain/ # Pure value objects, enums, contracts. Zero external dependencies.
├── Application/ # CQRS queries and commands plus handlers. May depend only on Domain.
├── Infrastructure/ # HTTP client, repositories, audit loggers, cache decorator. May depend on Application, Domain and framework types.
└── Presentation/ # Backend controller, Fluid templates, event listeners.
The CQRS split is shallow but consistent: every backend interaction goes through a *Query / *Command object plus its dedicated *Handler. Handlers receive their dependencies via constructor injection — final readonly throughout.
The HTTP client implements Psr\Http\Client\ClientInterface and is constructor-injected. Domain code is framework-free and trivially unit-testable.
The optional cache is a decorator over LapiDecisionRepository that is only wired into the DI container when moselwal/keyvalue-store is installed and cacheEnabled=1 is set (see Configuration/Services.php). When the package is absent, a NullDecisionCacheInvalidator no-op binding ensures the write handlers do not need any runtime branching.
Configuration: bouncer key and extension options
Generate the bouncer API key inside the CrowdSec container:
cscli bouncers add typo3-bridge
The bridge looks up the key in this exact order:
- Docker secret file
/run/secrets/crowdsec_api_key— recommended for production, SOPS-encrypted and mounted as a Docker secret. - Environment variable
CROWDSEC_LAPI_KEY. $GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['crowdsec_bridge']['lapiKey']— development only, never use in production.
If none of these resolves, the module surfaces a clear configuration error on first access.
Extension configuration
Managed via Admin Tools → Settings → Extension Configuration → crowdsec_bridge.
lapiBaseUrlDefault
crowdsec. Full URL to the CrowdSec Local API. Must be reachable from the TYPO3 container.lapiTimeoutDefault
5. HTTP timeout in seconds for all LAPI calls.allowDeleteForeignDecisionsDefault
0. When1, TYPO3 admins (not regular operators) may also delete decisions created by CrowdSec orcscli. Regularcrowdsec_operatormembers are never allowed to delete foreign decisions regardless of this setting.cacheEnabledDefault
1. Enables the decision-list cache. Requiresmoselwal/keyvalue-storeto be installed; without it the setting has no effect.cacheTtlDefault
10seconds. Cache lifetime. Valid range 1–300; out-of-range values are clamped at construction time and a notice is written to the TYPO3 log.
Permissions and security model
The bridge does not auto-create backend groups. Three roles are created once under System → Backend Users → Groups and granted module access:
crowdsec_viewer— read-onlyDecisions list, alerts list, IP quick-check, LAPI status.
crowdsec_operator— operatorAll viewer rights, plus add new decisions and delete own
typo3-origin decisions.- TYPO3 admin (
$BE_USER->isAdmin() === true) All operator rights, plus delete foreign-origin decisions when
allowDeleteForeignDecisions=1.
All access checks run server-side. The UI hides buttons that the current user is not permitted to use; the controller additionally enforces every check, so direct URL manipulation results in HTTP 403.
Defence in depth against writes
- Module visibility — TYPO3 module-access ACL hides the module from users without the assigned group.
- Controller entry — every action calls
OperatorPermissionsFactory::build()and rejects requests that fail the relevantassertCan*. - CSRF — every write endpoint validates the TYPO3 backend form token; missing or invalid tokens result in HTTP 403 and a
crowdsec_bridge:csrf_deniedaudit entry. - Double-submit — add and delete forms include a server-stored submit token, consumed on first POST; a duplicate triggers
crowdsec_bridge:duplicate_submit. - Input validation — all values are normalised and validated in domain value objects (
IpAddress,Duration,DecisionScope,DecisionType,Reason) before any LAPI call. - Foreign-origin protection —
OperatorPermissions::assertCanDelete(Decision)enforces origin rules server-side. - Rate limiting — no more than one writing LAPI call per HTTP request, no bulk endpoints (operators wanting batch operations must use
cscli).
No secret material is ever rendered into the browser. The bouncer API key lives in PHP-side state only, sourced from a Docker secret, environment variable, or TYPO3_CONF_VARS (in that order).
Audit trail in sys_log
The bridge writes structured entries to sys_log. Filter the System → Log module by these type values to inspect activity:
crowdsec_bridge:quickcheckEvery IP quick-check (hit or miss). Channel
security, severityinfo.crowdsec_bridge:decision_addSuccessful or failed add attempts. Channel
security, severityinfoorwarning.crowdsec_bridge:decision_deleteSuccessful, not-found, or failed delete attempts. Channel
security, severityinfoorwarning.crowdsec_bridge:write_deniedWrite attempt by a user without the operator role. Channel
security, severitywarning.crowdsec_bridge:delete_deniedDelete attempt against a foreign-origin row without admin override. Channel
security, severitywarning.crowdsec_bridge:csrf_deniedWrite attempt with missing or invalid form token. Channel
security, severitywarning.crowdsec_bridge:duplicate_submitRe-submission of an already-consumed form token. Channel
security, severitywarning.
The full audit context (action, status, decision data, original origin, latency, error message) is JSON-serialised into the log_data column — ready for downstream analysis.
Optional decision-list cache
With moselwal/keyvalue-store installed and cacheEnabled=1, the bridge caches paginated decision-list responses with a short TTL (default 10 s). Typical impact: repeated F5 reloads stay below 50 ms.
Invalidation with a generation counter
Atomic INCR on a single KVS key. After every successful add or delete the counter is incremented — all previously cached keys become unreachable in O(1) without scanning. Old entries expire via TTL.
Failure handling
Any KVS error (backend down, connection timeout, malformed payload) silently degrades to direct LAPI calls. The first failure of a request is logged once at warning level into the TYPO3 logger; subsequent failures within the same request are suppressed to avoid log spam.
What is not cached
findById— every individual decision lookup goes straight to LAPI to keep the delete-origin snapshot honest.findByIp(quick-check) — operators expect this to reflect the live LAPI state.- LAPI heartbeat — the status banner must always be current.
- Alerts — CrowdSec-internal, not cached in this release.
Installation
composer require moselwal/crowdsec-bridge
vendor/bin/typo3 extension:setup
Optionally enable the decision-list cache:
composer require moselwal/keyvalue-store
vendor/bin/typo3 cache:flush
Docker compose example
A minimal snippet wiring the bridge to a CrowdSec container and an optional Redis cache:
services:
app:
image: typo3:14
depends_on:
- crowdsec
- redis
networks:
- internal
secrets:
- crowdsec_api_key
environment:
CROWDSEC_LAPI_KEY_FILE: /run/secrets/crowdsec_api_key
crowdsec:
image: crowdsecurity/crowdsec:latest
networks:
- internal
expose:
- "8080"
redis:
image: redis:7-alpine
networks:
- internal
expose:
- "6379"
networks:
internal:
secrets:
crowdsec_api_key:
file: ./secrets/dev/crowdsec_api_key
Production: encrypt the secret file with SOPS and age, mount through Docker secrets, and do not expose either Redis or CrowdSec ports outside the internal network.
Bringing CrowdSec into the TYPO3 backend?
If you already run CrowdSec and want to operate decisions and alerts directly inside the TYPO3 backend — without container shell, with clear role separation, foreign-origin protection and a complete audit trail in sys_log — talk to me. I coordinate bouncer setup, backend groups, the optional decision cache and operator onboarding.
Or email us directly: kontakt@moselwal.de
Where I deploy this
This package belongs to the operator layer of any TYPO3 platform running behind Caddy with CrowdSec — decisions and alerts directly in the backend instead of in the container shell. In the managed variant: AI-Ready CMS as a Service with a CrowdSec bouncer, decision cache via keyvalue-store and properly configured backend roles.