Skip to content

Commit 59903c1

Browse files
Merge commit from fork
* formatting * slowapi to use same redis connection pool as app * feat: temporary modification to spin up two local webservers * adds connection_url_encoded field to Redis config * unencode redis pw for slowapi * actually using unencoded pass * reverse logic to add rate limiting middleware * feat: add enhanced nginx logging * refactor rate limiting into 2 shared buckets, add validation for insecure rate limit headers * explicit annotations for rate limiting buckets * adds missing annotations to endpoints * adds annotations to health endpoints * refactor: make nginx optional in nox -s dev * refactor: restructure docker-compose and nox dev session for single/nginx modes * fix: re-add mistaken removed env vars from fides service * fix: revert other changes unrelated to nginx * Revert "adds annotations to health endpoints" This reverts commit af5659c. * Revert "adds missing annotations to endpoints" This reverts commit f69150d. * Revert "explicit annotations for rate limiting buckets" This reverts commit d849d82. * remove public req rate limit env var, implement shared bucket * address CR feedback, notably adding custom exception handler for rate limit hit, and adding clean_ip logic to better handle various use cases * use existing lib to help parse ip * restricts ip validation to only one ip address and possibly a port, no handling of special characters * Adds changelog * adds safe and unsafe ip header extraction such that we do not throw errors from the key_func * fix improts * update env var usage --------- Co-authored-by: Dave Quinlan <[email protected]>
1 parent 64ce86a commit 59903c1

File tree

15 files changed

+703
-29
lines changed

15 files changed

+703
-29
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Changes can also be flagged with a GitHub label for tracking purposes. The URL o
6464

6565
### Security
6666
- Added stricter rate limiting to authentication endpoints to mitigate against brute force attacks. [CVE-2025-57815](https://github.com/ethyca/fides/security/advisories/GHSA-7q62-r88r-j5gw)
67+
- Adds Redis-driven rate limiting across all endpoints [CVE-2025-57816](https://github.com/ethyca/fides/security/advisories/GHSA-fq34-xw6c-fphf)
6768

6869
## [2.68.0](https://github.com/ethyca/fides/compare/2.67.2...2.68.0)
6970

docker-compose.yml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,100 @@ services:
221221
- CELERY_BROKER_URL=redis://:redispassword@redis:6379/0
222222
- CELERY_RESULT_BACKEND=redis://:redispassword@redis:6379/0
223223

224+
# Cluster services for nginx load balancing
225+
fides-1:
226+
container_name: fides-1
227+
image: ethyca/fides:local
228+
command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src --reload-dir data --reload-include='*.yml' fides.api.main:app
229+
healthcheck:
230+
test: ["CMD", "curl", "-f", "http://0.0.0.0:8080/health"]
231+
interval: 20s
232+
timeout: 5s
233+
retries: 10
234+
ports:
235+
- "8081:8080"
236+
depends_on:
237+
fides-db:
238+
condition: service_healthy
239+
redis:
240+
condition: service_started
241+
expose:
242+
- 8080
243+
env_file:
244+
- .env
245+
environment:
246+
FIDES__CONFIG_PATH: ${FIDES__CONFIG_PATH:-/fides/.fides/fides.toml}
247+
FIDES__CLI__ANALYTICS_ID: ${FIDES__CLI__ANALYTICS_ID-}
248+
FIDES__CLI__SERVER_HOST: "fides"
249+
FIDES__CLI__SERVER_PORT: "8080"
250+
FIDES__DATABASE__SERVER: "fides-db"
251+
FIDES__DEV_MODE: "True"
252+
FIDES__LOGGING__COLORIZE: "True"
253+
FIDES__USER__ANALYTICS_OPT_OUT: "True"
254+
FIDES__SECURITY__BASTION_SERVER_HOST: ${FIDES__SECURITY__BASTION_SERVER_HOST-}
255+
FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME: ${FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME-}
256+
FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY: ${FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY-}
257+
SAAS_OP_SERVICE_ACCOUNT_TOKEN: ${SAAS_OP_SERVICE_ACCOUNT_TOKEN-}
258+
SAAS_SECRETS_OP_VAULT_ID: ${SAAS_SECRETS_OP_VAULT_ID-}
259+
volumes:
260+
- type: bind
261+
source: .
262+
target: /fides
263+
read_only: False
264+
265+
fides-2:
266+
container_name: fides-2
267+
image: ethyca/fides:local
268+
command: uvicorn --host 0.0.0.0 --port 8080 --reload --reload-dir src --reload-dir data --reload-include='*.yml' fides.api.main:app
269+
healthcheck:
270+
test: ["CMD", "curl", "-f", "http://0.0.0.0:8080/health"]
271+
interval: 20s
272+
timeout: 5s
273+
retries: 10
274+
ports:
275+
- "8082:8080"
276+
depends_on:
277+
fides-db:
278+
condition: service_healthy
279+
redis:
280+
condition: service_started
281+
expose:
282+
- 8080
283+
env_file:
284+
- .env
285+
environment:
286+
FIDES__CONFIG_PATH: ${FIDES__CONFIG_PATH:-/fides/.fides/fides.toml}
287+
FIDES__CLI__ANALYTICS_ID: ${FIDES__CLI__ANALYTICS_ID-}
288+
FIDES__CLI__SERVER_HOST: "fides"
289+
FIDES__CLI__SERVER_PORT: "8080"
290+
FIDES__DATABASE__SERVER: "fides-db"
291+
FIDES__DEV_MODE: "True"
292+
FIDES__LOGGING__COLORIZE: "True"
293+
FIDES__USER__ANALYTICS_OPT_OUT: "True"
294+
FIDES__SECURITY__BASTION_SERVER_HOST: ${FIDES__SECURITY__BASTION_SERVER_HOST-}
295+
FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME: ${FIDES__SECURITY__BASTION_SERVER_SSH_USERNAME-}
296+
FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY: ${FIDES__SECURITY__BASTION_SERVER_SSH_PRIVATE_KEY-}
297+
SAAS_OP_SERVICE_ACCOUNT_TOKEN: ${SAAS_OP_SERVICE_ACCOUNT_TOKEN-}
298+
SAAS_SECRETS_OP_VAULT_ID: ${SAAS_SECRETS_OP_VAULT_ID-}
299+
volumes:
300+
- type: bind
301+
source: .
302+
target: /fides
303+
read_only: False
304+
305+
fides-proxy:
306+
container_name: fides-proxy
307+
image: nginx:latest
308+
ports:
309+
- "8083:8080"
310+
volumes:
311+
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
312+
depends_on:
313+
fides-1:
314+
condition: service_healthy
315+
fides-2:
316+
condition: service_healthy
317+
224318
volumes:
225319
postgres: null
226320

docker/nginx/nginx.conf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
events {}
2+
3+
http {
4+
5+
log_format upstream_log '$remote_addr - $remote_user [$time_local] '
6+
'"$request" $status $body_bytes_sent '
7+
'"$http_referer" "$http_user_agent" '
8+
'upstream_addr="$upstream_addr" '
9+
'upstream_status="$upstream_status"';
10+
11+
upstream fides {
12+
server fides-1:8080;
13+
server fides-2:8080;
14+
}
15+
16+
server {
17+
listen 8080;
18+
access_log /dev/stdout upstream_log;
19+
20+
location / {
21+
proxy_pass http://fides;
22+
}
23+
}
24+
}

noxfiles/dev_nox.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def dev(session: Session) -> None:
5454
- workers-all = Run all available Fides workers (see below)
5555
- flower = Run Flower monitoring dashboard for Celery
5656
- child = Run a Fides child node
57+
- nginx = Run two Fides webservers with nginx load balancer proxy
5758
- <datastore(s)> = Run a test datastore (e.g. 'mssql', 'mongodb')
5859
5960
To run specific workers only, use any of the following posargs:
@@ -111,7 +112,20 @@ def dev(session: Session) -> None:
111112

112113
open_shell = "shell" in session.posargs
113114
remote_debug = "remote_debug" in session.posargs
114-
if not datastores:
115+
use_nginx = "nginx" in session.posargs
116+
117+
if use_nginx:
118+
# Run two Fides webservers with nginx load balancer proxy
119+
session.run(
120+
"docker",
121+
"compose",
122+
"up",
123+
"fides-1",
124+
"fides-2",
125+
"fides-proxy",
126+
external=True,
127+
)
128+
elif not datastores:
115129
if open_shell:
116130
session.run(*START_APP, external=True)
117131
session.log("~~Remember to login with `fides user login`!~~")

src/fides/api/api/v1/endpoints/dsr_package_link.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from fides.api.schemas.storage.storage import StorageType
2222
from fides.api.service.storage.streaming.s3 import S3StorageClient
2323
from fides.api.util.api_router import APIRouter
24-
from fides.api.util.endpoint_utils import fides_limiter
24+
from fides.api.util.rate_limit import fides_limiter
2525
from fides.common.api.v1.urn_registry import PRIVACY_CENTER_DSR_PACKAGE, V1_URL_PREFIX
2626
from fides.config import CONFIG
2727

@@ -62,7 +62,7 @@ def raise_error(status_code: int, detail: str) -> None:
6262
PRIVACY_CENTER_DSR_PACKAGE,
6363
status_code=HTTP_302_FOUND,
6464
)
65-
@fides_limiter.limit(CONFIG.security.public_request_rate_limit)
65+
@fides_limiter.limit(CONFIG.security.request_rate_limit)
6666
def get_access_results_urls(
6767
privacy_request_id: str,
6868
token: str,

src/fides/api/api/v1/endpoints/oauth_endpoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
)
3838
from fides.api.util.api_router import APIRouter
3939
from fides.api.util.connection_util import connection_status
40-
from fides.api.util.endpoint_utils import fides_limiter
40+
from fides.api.util.rate_limit import fides_limiter
4141
from fides.common.api.scope_registry import (
4242
CLIENT_CREATE,
4343
CLIENT_DELETE,

src/fides/api/api/v1/endpoints/user_endpoints.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
)
6060
from fides.api.service.deps import get_user_service
6161
from fides.api.util.api_router import APIRouter
62-
from fides.api.util.endpoint_utils import fides_limiter
62+
from fides.api.util.rate_limit import fides_limiter
6363
from fides.common.api.scope_registry import (
6464
SCOPE_REGISTRY,
6565
SYSTEM_MANAGER_DELETE,

src/fides/api/app_setup.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,13 @@
4848
from fides.api.util.api_router import APIRouter
4949
from fides.api.util.cache import get_cache
5050
from fides.api.util.consent_util import create_default_tcf_purpose_overrides_on_startup
51-
from fides.api.util.endpoint_utils import fides_limiter
5251
from fides.api.util.errors import FidesError
5352
from fides.api.util.logger import setup as setup_logging
53+
from fides.api.util.rate_limit import (
54+
RateLimitIPValidationMiddleware,
55+
fides_limiter,
56+
is_rate_limit_enabled,
57+
)
5458
from fides.config import CONFIG
5559
from fides.config.config_proxy import ConfigProxy
5660

@@ -88,7 +92,17 @@ def create_fides_app(
8892
for handler in ExceptionHandlers.get_handlers():
8993
# Starlette bug causing this to fail mypy
9094
fastapi_app.add_exception_handler(RedisNotConfigured, handler) # type: ignore
91-
fastapi_app.add_middleware(SlowAPIMiddleware)
95+
96+
if is_rate_limit_enabled:
97+
# Validate header before SlowAPI processes the request
98+
fastapi_app.add_middleware(RateLimitIPValidationMiddleware)
99+
# Required for default rate limiting to work
100+
fastapi_app.add_middleware(SlowAPIMiddleware)
101+
else:
102+
logger.warning(
103+
"Rate limiting client IPs is disabled because the FIDES__SECURITY__RATE_LIMIT_CLIENT_IP_HEADER env var is not configured."
104+
)
105+
92106
fastapi_app.add_middleware(
93107
GZipMiddleware, minimum_size=1000, compresslevel=5
94108
) # minimum_size is in bytes

src/fides/api/main.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
from fideslog.sdk.python.event import AnalyticsEvent
1919
from loguru import logger
2020
from pyinstrument import Profiler
21+
from slowapi import _rate_limit_exceeded_handler
22+
from slowapi.errors import RateLimitExceeded
2123
from starlette.background import BackgroundTask
2224
from starlette.status import HTTP_422_UNPROCESSABLE_ENTITY
2325
from uvicorn import Config, Server
@@ -60,6 +62,7 @@
6062
)
6163
from fides.api.util.endpoint_utils import API_PREFIX
6264
from fides.api.util.logger import _log_exception
65+
from fides.api.util.rate_limit import safe_rate_limit_key
6366
from fides.cli.utils import FIDES_ASCII_ART
6467
from fides.config import CONFIG, check_required_webserver_config_values
6568

@@ -388,3 +391,22 @@ async def request_validation_exception_handler(
388391
"detail": jsonable_encoder(exc.errors(), exclude={"input", "url", "ctx"})
389392
},
390393
)
394+
395+
396+
@app.exception_handler(RateLimitExceeded)
397+
async def rate_limit_handler(request: Request, exc: RateLimitExceeded) -> Response:
398+
"""Log rate limit violations and delegate to default handler."""
399+
client_ip = safe_rate_limit_key(
400+
request
401+
) # non exception-raising, falls back to source IP
402+
403+
# Log the rate limit event
404+
logger.warning(
405+
"Rate limit exceeded - IP: %s, Path: %s, Method: %s",
406+
client_ip,
407+
request.url.path,
408+
request.method,
409+
)
410+
411+
# Use the default handler to generate the proper response
412+
return _rate_limit_exceeded_handler(request, exc)

src/fides/api/util/endpoint_utils.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
from fastapi import HTTPException
77
from fideslang import FidesModelType
8-
from slowapi import Limiter
9-
from slowapi.util import get_remote_address # type: ignore
108
from sqlalchemy.ext.asyncio import AsyncSession
119
from starlette.status import HTTP_400_BAD_REQUEST
1210

@@ -23,7 +21,6 @@
2321
ORGANIZATION,
2422
SYSTEM,
2523
)
26-
from fides.config import CONFIG
2724

2825
from fides.api.models.sql_models import ( # type: ignore[attr-defined] # isort: skip
2926
ModelWithDefaultField,
@@ -44,16 +41,6 @@
4441
"system": SYSTEM,
4542
}
4643

47-
# Used for rate limiting with Slow API
48-
# Decorate individual routes to deviate from the default rate limits
49-
fides_limiter = Limiter(
50-
default_limits=[CONFIG.security.request_rate_limit],
51-
headers_enabled=True,
52-
key_prefix=CONFIG.security.rate_limit_prefix,
53-
key_func=get_remote_address,
54-
retry_after="http-date",
55-
)
56-
5744

5845
async def forbid_if_editing_is_default(
5946
sql_model: Base,

0 commit comments

Comments
 (0)