Extract real client IP from proxy headers instead of Docker bridge IP
Nginx already forwards X-Forwarded-For and X-Real-IP, but the backend read request.client.host directly — always returning 172.18.0.x. Added get_client_ip() helper to audit service; updated all 13 call sites. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f8c2df9328
commit
21aa670a39
@ -48,7 +48,7 @@ from app.schemas.admin import (
|
|||||||
UserListItem,
|
UserListItem,
|
||||||
UserListResponse,
|
UserListResponse,
|
||||||
)
|
)
|
||||||
from app.services.audit import log_audit_event
|
from app.services.audit import get_client_ip, log_audit_event
|
||||||
from app.services.auth import hash_password
|
from app.services.auth import hash_password
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@ -228,7 +228,7 @@ async def create_user(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=new_user.id,
|
target_id=new_user.id,
|
||||||
detail={"username": new_user.username, "role": new_user.role},
|
detail={"username": new_user.username, "role": new_user.role},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -290,7 +290,7 @@ async def update_user_role(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"old_role": old_role, "new_role": data.role, "sessions_revoked": revoked},
|
detail={"old_role": old_role, "new_role": data.role, "sessions_revoked": revoked},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -332,7 +332,7 @@ async def reset_user_password(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"sessions_revoked": revoked},
|
detail={"sessions_revoked": revoked},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -385,7 +385,7 @@ async def disable_user_mfa(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"sessions_revoked": revoked},
|
detail={"sessions_revoked": revoked},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -420,7 +420,7 @@ async def toggle_mfa_enforce(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"enforce": data.enforce},
|
detail={"enforce": data.enforce},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -462,7 +462,7 @@ async def toggle_user_active(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"sessions_revoked": revoked},
|
detail={"sessions_revoked": revoked},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -494,7 +494,7 @@ async def revoke_user_sessions(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"sessions_revoked": revoked},
|
detail={"sessions_revoked": revoked},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
@ -545,7 +545,7 @@ async def delete_user(
|
|||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
target_id=user_id,
|
target_id=user_id,
|
||||||
detail={"user_id": user_id, "username": deleted_username},
|
detail={"user_id": user_id, "username": deleted_username},
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
# Flush audit + session revocation within the same transaction
|
# Flush audit + session revocation within the same transaction
|
||||||
await db.flush()
|
await db.flush()
|
||||||
@ -652,7 +652,7 @@ async def update_system_config(
|
|||||||
action="admin.config_updated",
|
action="admin.config_updated",
|
||||||
actor_id=actor.id,
|
actor_id=actor.id,
|
||||||
detail=changes,
|
detail=changes,
|
||||||
ip=request.client.host if request.client else None,
|
ip=get_client_ip(request),
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@ -43,7 +43,7 @@ from app.services.auth import (
|
|||||||
create_mfa_token,
|
create_mfa_token,
|
||||||
create_mfa_enforce_token,
|
create_mfa_enforce_token,
|
||||||
)
|
)
|
||||||
from app.services.audit import log_audit_event
|
from app.services.audit import get_client_ip, log_audit_event
|
||||||
from app.config import settings as app_settings
|
from app.config import settings as app_settings
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@ -275,7 +275,7 @@ async def setup(
|
|||||||
|
|
||||||
await _create_user_defaults(db, new_user.id)
|
await _create_user_defaults(db, new_user.id)
|
||||||
|
|
||||||
ip = request.client.host if request.client else "unknown"
|
ip = get_client_ip(request)
|
||||||
user_agent = request.headers.get("user-agent")
|
user_agent = request.headers.get("user-agent")
|
||||||
_, token = await _create_db_session(db, new_user, ip, user_agent)
|
_, token = await _create_db_session(db, new_user, ip, user_agent)
|
||||||
_set_session_cookie(response, token)
|
_set_session_cookie(response, token)
|
||||||
@ -307,7 +307,7 @@ async def login(
|
|||||||
HTTP 403 — account disabled (is_active=False)
|
HTTP 403 — account disabled (is_active=False)
|
||||||
HTTP 423 — account locked
|
HTTP 423 — account locked
|
||||||
"""
|
"""
|
||||||
client_ip = request.client.host if request.client else "unknown"
|
client_ip = get_client_ip(request)
|
||||||
|
|
||||||
result = await db.execute(select(User).where(User.username == data.username))
|
result = await db.execute(select(User).where(User.username == data.username))
|
||||||
user = result.scalar_one_or_none()
|
user = result.scalar_one_or_none()
|
||||||
@ -438,7 +438,7 @@ async def register(
|
|||||||
|
|
||||||
await _create_user_defaults(db, new_user.id)
|
await _create_user_defaults(db, new_user.id)
|
||||||
|
|
||||||
ip = request.client.host if request.client else "unknown"
|
ip = get_client_ip(request)
|
||||||
user_agent = request.headers.get("user-agent")
|
user_agent = request.headers.get("user-agent")
|
||||||
|
|
||||||
await log_audit_event(
|
await log_audit_event(
|
||||||
|
|||||||
@ -35,6 +35,7 @@ from app.models.session import UserSession
|
|||||||
from app.models.totp_usage import TOTPUsage
|
from app.models.totp_usage import TOTPUsage
|
||||||
from app.models.backup_code import BackupCode
|
from app.models.backup_code import BackupCode
|
||||||
from app.routers.auth import get_current_user, _set_session_cookie
|
from app.routers.auth import get_current_user, _set_session_cookie
|
||||||
|
from app.services.audit import get_client_ip
|
||||||
from app.services.auth import (
|
from app.services.auth import (
|
||||||
verify_password_with_upgrade,
|
verify_password_with_upgrade,
|
||||||
hash_password,
|
hash_password,
|
||||||
@ -164,7 +165,7 @@ async def _create_full_session(
|
|||||||
"""Create a UserSession row and return the signed cookie token."""
|
"""Create a UserSession row and return the signed cookie token."""
|
||||||
session_id = uuid.uuid4().hex
|
session_id = uuid.uuid4().hex
|
||||||
expires_at = datetime.now() + timedelta(days=app_settings.SESSION_MAX_AGE_DAYS)
|
expires_at = datetime.now() + timedelta(days=app_settings.SESSION_MAX_AGE_DAYS)
|
||||||
ip = request.client.host if request.client else None
|
ip = get_client_ip(request)
|
||||||
user_agent = request.headers.get("user-agent")
|
user_agent = request.headers.get("user-agent")
|
||||||
|
|
||||||
db_session = UserSession(
|
db_session = UserSession(
|
||||||
|
|||||||
@ -1,8 +1,21 @@
|
|||||||
import json
|
import json
|
||||||
|
from fastapi import Request
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from app.models.audit_log import AuditLog
|
from app.models.audit_log import AuditLog
|
||||||
|
|
||||||
|
|
||||||
|
def get_client_ip(request: Request) -> str:
|
||||||
|
"""Extract the real client IP from proxy headers, falling back to direct connection."""
|
||||||
|
forwarded_for = request.headers.get("x-forwarded-for")
|
||||||
|
if forwarded_for:
|
||||||
|
# X-Forwarded-For can be a comma-separated list; first entry is the original client
|
||||||
|
return forwarded_for.split(",")[0].strip()
|
||||||
|
real_ip = request.headers.get("x-real-ip")
|
||||||
|
if real_ip:
|
||||||
|
return real_ip.strip()
|
||||||
|
return request.client.host if request.client else "unknown"
|
||||||
|
|
||||||
|
|
||||||
async def log_audit_event(
|
async def log_audit_event(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
action: str,
|
action: str,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user