fix: harden Redis-backed delivery, public APIs, and production service settings - #71
Open
Felipequesada21 wants to merge 21 commits into
Open
fix: harden Redis-backed delivery, public APIs, and production service settings#71Felipequesada21 wants to merge 21 commits into
Felipequesada21 wants to merge 21 commits into
Conversation
Add outbound HTTP timeouts, redact credentials from logged feed URLs, bound Redis stream growth, remove invalid API filter fields, and log silent decoding failures. - api: drop filterset_fields that do not exist on FareAttribute and reduce FareRule to route_id. Both routes returned HTTP 500 for any query-parameter request because django-filter validates the whole field list before applying any filter. - infobus: add redact_url helper and HTTP_REQUEST_TIMEOUT_SECONDS, REDIS_EVENTS_STREAM_MAXLEN and REDIS_DEAD_LETTER_STREAM_MAXLEN settings. - engine: add the missing timeout to the vehicle positions request and replace the two repeated literals with the setting. Redact URLs in the three error logs. - feed: add timeouts to the schedule HEAD and GET requests, use an aware timestamp in the import log, and redact the schedule URL. - runs, updates: bound the events and dead-letter streams with approximate MAXLEN. Measured 4.1M entries accumulated over 29 days with no trimming policy. - updates: log a warning when a stop time updates document fails to decode, instead of silently returning an empty list. Two tests added. Test suites: updates 84, runs 12, engine 8, all passing. No migrations, no model changes, no public contract additions.
SECURE_CONTENT_TYPE_NOSNIFF and SECURE_REFERRER_POLICY were declared with config() defaults below Django's own, which actively suppressed two response headers that the framework emits out of the box. - SECURE_CONTENT_TYPE_NOSNIFF: default False -> True (Django default) - SECURE_REFERRER_POLICY: default None -> "same-origin" (Django default) Both settings remain overridable through the environment; no .env file is modified. The remaining seven settings in the block keep insecure defaults on purpose, so that local development over plain HTTP is not broken; their production values belong in .env.prod. Verified in dev after restarting orchestrator: /health/ now returns X-Content-Type-Options: nosniff and Referrer-Policy: same-origin. manage.py check --deploy drops security.W006 and security.W022 (8 issues -> 6). Test suites unchanged: updates 84, runs 12, engine 8.
Three independent, declarative changes to compose.prod.yml. No production deployment exists yet, so these were verified by configuration render only. - Pin floating image tags to exact versions, verified to exist in the registry: rabbitmq:4-management -> 4.3.5-management, redis:7-alpine -> 7.4.11-alpine, nginx:alpine -> 1.31.4-alpine. Redis deliberately stays on the 7.x line; the move to 8.x is a separate change that must ship together with the streams-consumer service. The knowledge service builds its image locally and has no registry tag to pin. - Add start_period: 60s to the broker, database and memory healthchecks. These had no start period, leaving roughly 50s of grace. The setting only makes a healthcheck more permissive and is a precondition for adding a healthcheck to orchestrator, which takes about 67s to boot. - Remove the traefik_proxy network and the eight Traefik labels from the broker service, so the RabbitMQ management UI is no longer published on a public domain. The service stays on the internal network; engine and scheduler reach it unchanged through depends_on. This does not retire RabbitMQ, which remains pending a product decision. Verified with docker compose -f compose.prod.yml config: render completes, broker declares only the internal network and no labels, orchestrator, user-interface, context, knowledge and docs keep traefik_proxy, and the BROKER_DOMAIN warning is gone because the labels that referenced it are.
- Replace API ModelViewSets with ReadOnlyModelViewSets - Restrict the schema endpoint to GET requests - Configure anonymous and scoped throttling for realtime and geometry endpoints - Add pagination defaults for REST responses - Configure Redis-backed Django cache with optional password support
Add API tests for read-only router methods, paginated list responses, geometry throttling, and schema GET-only access. Isolate cache-backed throttling with LocMemCache to avoid development Redis writes.
Adds the streams-consumer service to the production manifest, following the engine/scheduler pattern, and moves memory from redis:7.4.11-alpine to redis:8.10.1-alpine, which bundles RedisJSON. Both changes ship together: the consumer path calls RedisJSON commands, so deploying the consumer against Redis 7 would fail after dispatch and leave every entry pending, since XACK happens after dispatch. The consumer still cannot authenticate against a password-protected Redis (backend/updates/client.py:83-88), and the Channels layer omits credentials (backend/infobus/settings.py:226-233). The service must not be deployed until REDIS_PASSWORD reaches both.
Propaga REDIS_PASSWORD al broker de Celery y a Channels; conserva el comportamiento de desarrollo cuando la contraseña está vacía.
Pass REDIS_PASSWORD (or None when unset) to the ten direct Redis clients in runs and updates, preserving existing decode_responses behavior.
Restore GeoJSON pagination for geometry endpoints and align route-stop throttling. Harden Redis configuration across dev and production: - require Redis authentication in dev and fail fast in production - add resilient DRF throttles that fail open if Redis is unavailable - respect client IPs behind Traefik with DRF_NUM_PROXIES Prevent feed URL leaks in error logs, configure drf-spectacular's schema class, and document new Redis, throttling, pagination, and stream settings. Add coverage for URL redaction edge cases.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch addresses production-readiness gaps around unauthenticated Redis access, unbounded Redis Streams, mutable public REST resources, and fragile external-feed requests. It also makes the production Compose topology more explicit by adding the stream consumer, pinning selected image tags, and keeping the RabbitMQ management service on the internal network only.
The system now carries configured Redis credentials through Celery, Channels, cache, and direct Redis clients; applies stream caps; exposes registered model APIs as read-only; throttles public endpoints; and bounds external requests while redacting credentials and query strings from feed URLs in logs. These changes are covered by the targeted application suites in this branch.
What changed
Redis credentials and stream bounds
backend/infobus/settings.py:139,backend/infobus/settings.py:193,backend/infobus/settings.py:230,backend/runs/services/lifecycle.py:56,backend/updates/consumers.py:17backend/runs/services/lifecycle.py:549,backend/runs/services/state.py:115,backend/updates/client.py:43compose.prod.yml:115,compose.prod.yml:140,compose.prod.yml:154,compose.prod.yml:184,compose.prod.yml:257Public API hardening
ReadOnlyModelViewSet, preventing create, update, and delete actions through those public viewsets.backend/api/views.py:59,backend/api/views.py:265,backend/api/views.py:443,backend/api/tests.py:83backend/infobus/settings.py:211,backend/api/views.py:72,backend/api/views.py:295,backend/api/views.py:507,backend/api/tests.py:142Feed polling and diagnostic safety
backend/infobus/settings.py:174,backend/engine/tasks.py:94,backend/engine/tasks.py:99,backend/feed/services/schedule.py:49,backend/feed/services/schedule.py:67,backend/infobus/utils.py:6backend/updates/builders/stop/stop_time_updates.py:41,backend/updates/builders/stop/stop_time_updates.py:49,backend/updates/builders/stop/stop_time_updates.py:58Verification
updates87,runs13,engine9, andapi5, for 114 total tests. No test failures occurred.ruff check .is failing with 54 errors, includingF401 django.shortcuts.renderatupdates/views.py:1; 10 findings are marked fixable by Ruff.docker compose -f compose.prod.yml config --quietpassed but reported unresolved variable namesREDIS_PASSWORD,CONTEXT_DOMAIN,KNOWLEDGE_DOMAIN,ORCHESTRATOR_DOMAIN,DOCS_DOMAIN, andUI_DOMAIN.docker compose -f compose.dev.yml config --quietpassed without warnings.mbta.route.vehicle_positions.by_route.Red.by_direction.0: an initial snapshot was followed by messages at approximately 28-second intervals.Known issues not addressed in this PR
URLRouter, and the consumer accepts connections immediately; neither an origin validator nor an authentication wrapper is present.backend/infobus/asgi.py:22,backend/updates/consumers.py:39,backend/updates/routing.py:5This requires an explicit access-control design and is outside this production-settings change set.transit_systemtopic segment is not validated againstTransitSystem.code.TopicKey.parsechecks topic shape and non-empty segments only, while projection validation does not perform this database lookup; this batch also validated a syntactically valid topic with an unknown system successfully.backend/updates/topics.py:20,backend/updates/topics.py:37,backend/updates/planner.py:25,backend/feed/models.py:38Adding that lookup changes subscription semantics and is outside the branch.Feedrecords have a nulltransit_system. The model permits the nullable relation, and the read-only count returned 9.backend/feed/models.py:121,backend/feed/models.py:128Backfilling or constraining existing schedule data is outside this branch.FeedMessage.timestampusesauto_now. This records save time rather than an immutable ingestion timestamp.backend/feed/models.py:143,backend/feed/models.py:157Changing the data model and its historical meaning is outside this PR.service_idcommented out.backend/feed/services/queries.py:191Restoring service filtering needs schedule-data validation and is outside this branch.time_limit, retry, or backoff configuration. The task decorators inengine/tasks.pyare bare@shared_taskdeclarations, and a repository search found no task-leveltime_limit, retry, or backoff configuration.backend/engine/tasks.py:46,backend/engine/tasks.py:67,backend/engine/tasks.py:127Delivery policy requires separate operational design./health/does not verify dependencies. It returnsOKwithout checking PostgreSQL, Redis, RabbitMQ, or upstream services.backend/infobus/urls.py:23Dependency health policy is outside the branch.feed_stoptimeupdatehas no retention path. The ingestion code bulk-creates rows and the scheduled task exports them to Parquet without deleting database rows.backend/feed/services/realtime.py:220,backend/engine/tasks.py:257,backend/feed/services/data.py:289The previously cited ~11.8 million rows/day estimate is not supported by this batch: a read-only count byFeedMessage.timestampmeasured 46,726,888 rows in the last 24 hours. Retention and archival policy are outside this PR.Additional API and Redis hardening
GeoJsonPagination, preservingFeatureCollectionresponses under globalpagination.
DRF_NUM_PROXIESis configured so throttling identifies real client IPs behind Traefik.REDIS_PASSWORDand authenticates its healthcheck; production Compose and Django both failfast if the password is missing.
RouteStopViewnow uses the geometry throttle scope..env.examplenow documents cache, timeout, throttling, pagination, proxy, and Redis Stream settings.Additional verification
infobusURL-redaction tests passed: 5 tests./api/geo-stops/and/api/geo-shapes/were verified to return GeoJSONFeatureCollectionpayloads.ConnectionError.manage.py check --deployno longer reportsdrf_spectacular.E001.ruff check .still reports 54 pre-existing findings; no new findings were introduced in modified files.Notes for the reviewer
Agregar una clave local real a .env:
REDIS_PASSWORD=una-clave-local-larga-y-unica
Luego recrear Redis y levantar de nuevo:
docker compose -f compose.dev.yml up -d --force-recreate memory
./scripts/dev.sh
REDIS_PASSWORDmust be defined on the server. With it unset, the production Compose command expands to--requirepass "", leaving Redis without authentication.compose.prod.yml:185