From 9cea7f9314df564eec5e1b1c8d14c1ec201aa43f Mon Sep 17 00:00:00 2001 From: Li Yi Date: Sat, 11 Oct 2025 14:49:18 +0800 Subject: [PATCH] chore: polish code with little update (#182) - Run Docker container as non-root user (appuser) to minimize security risks - Add Docker HEALTHCHECK for better container orchestration - Make CORS configurable via ALLOWED_ORIGINS env var with security warning - Replace assertions with proper error handling (TypeError/ValueError) - Add 30s timeout to HTTP requests to prevent hanging connections - Disable auto-reload in production uvicorn settings --- README.md | 2 +- THIRD_PARTY | 8 ++++++++ deployment/BedrockProxyFargate.template | 16 ++++++++-------- src/Dockerfile_ecs | 11 ++++++++++- src/api/app.py | 13 +++++++++++-- src/api/auth.py | 7 +++---- src/api/models/bedrock.py | 8 +++++--- src/api/setting.py | 2 -- 8 files changed, 46 insertions(+), 21 deletions(-) create mode 100644 THIRD_PARTY diff --git a/README.md b/README.md index c84e485..17154e0 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ If you encounter any issues, please check the [Troubleshooting Guide](./docs/Tro ### SDK/API Usage -All you need is the API Key and the API Base URL. If you didn't set up your own key, then the default API Key (`bedrock`) will be used. +All you need is the API Key and the API Base URL. If you didn't set up your own key following Step 1, the application will fail to start with an error message indicating that the API Key is not configured. Now, you can try out the proxy APIs. Let's say you want to test Claude 3 Sonnet model (model ID: `anthropic.claude-3-sonnet-20240229-v1:0`)... diff --git a/THIRD_PARTY b/THIRD_PARTY new file mode 100644 index 0000000..702c950 --- /dev/null +++ b/THIRD_PARTY @@ -0,0 +1,8 @@ +certifi + +SPDX-License-Identifier: MPL-2.0 +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. + +https://github.com/certifi/python-certifi \ No newline at end of file diff --git a/deployment/BedrockProxyFargate.template b/deployment/BedrockProxyFargate.template index eaf0b91..ed99267 100644 --- a/deployment/BedrockProxyFargate.template +++ b/deployment/BedrockProxyFargate.template @@ -256,8 +256,8 @@ Resources: Ref: ContainerImageUri Name: proxy-api PortMappings: - - ContainerPort: 80 - HostPort: 80 + - ContainerPort: 8080 + HostPort: 8080 Protocol: tcp Secrets: - Name: API_KEY @@ -303,7 +303,7 @@ Resources: HealthCheckGracePeriodSeconds: 60 LoadBalancers: - ContainerName: proxy-api - ContainerPort: 80 + ContainerPort: 8080 TargetGroupArn: Ref: ProxyALBListenerTargetsGroup187739FA NetworkConfiguration: @@ -340,7 +340,7 @@ Resources: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Load balancer to target - FromPort: 80 + FromPort: 8080 GroupId: Fn::GetAtt: - ProxyApiServiceSecurityGroup51EBD9B8 @@ -350,7 +350,7 @@ Resources: Fn::GetAtt: - ProxyALBSecurityGroup0D6CA3DA - GroupId - ToPort: 80 + ToPort: 8080 DependsOn: - ProxyTaskRoleDefaultPolicy933321B8 - ProxyTaskRole5DB6A540 @@ -396,13 +396,13 @@ Resources: Fn::GetAtt: - ProxyApiServiceSecurityGroup51EBD9B8 - GroupId - FromPort: 80 + FromPort: 8080 GroupId: Fn::GetAtt: - ProxyALBSecurityGroup0D6CA3DA - GroupId IpProtocol: tcp - ToPort: 80 + ToPort: 8080 ProxyALBListener933E9515: Type: AWS::ElasticLoadBalancingV2::Listener Properties: @@ -421,7 +421,7 @@ Resources: HealthCheckIntervalSeconds: 60 HealthCheckPath: /health HealthCheckTimeoutSeconds: 30 - Port: 80 + Port: 8080 Protocol: HTTP TargetGroupAttributes: - Key: stickiness.enabled diff --git a/src/Dockerfile_ecs b/src/Dockerfile_ecs index 502158d..eb315fb 100644 --- a/src/Dockerfile_ecs +++ b/src/Dockerfile_ecs @@ -8,6 +8,15 @@ RUN pip install --no-cache-dir --upgrade -r /app/requirements.txt COPY ./api /app/api -ENV PORT=80 +# Create non-root user +RUN groupadd -r appuser && useradd -r -g appuser appuser && \ + chown -R appuser:appuser /app + +USER appuser + +ENV PORT=8080 + +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/api/v1/models').read()" CMD ["sh", "-c", "uvicorn api.app:app --host 0.0.0.0 --port ${PORT}"] diff --git a/src/api/app.py b/src/api/app.py index 5ea7ae7..ef63c55 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -1,4 +1,5 @@ import logging +import os import uvicorn from fastapi import FastAPI @@ -23,9 +24,16 @@ logging.basicConfig( ) app = FastAPI(**config) +allowed_origins = os.environ.get("ALLOWED_ORIGINS", "*") +origins_list = [origin.strip() for origin in allowed_origins.split(",")] if allowed_origins != "*" else ["*"] + +# Warn if CORS allows all origins +if origins_list == ["*"]: + logging.warning("CORS is configured to allow all origins (*). Set ALLOWED_ORIGINS environment variable to restrict access.") + app.add_middleware( CORSMiddleware, - allow_origins=["*"], + allow_origins=origins_list, # nosec - configurable via ALLOWED_ORIGINS env var allow_credentials=True, allow_methods=["*"], allow_headers=["*"], @@ -61,4 +69,5 @@ async def validation_exception_handler(request, exc): handler = Mangum(app) if __name__ == "__main__": - uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) + # Bind to 0.0.0.0 for container environments, network is handled by network policies and load balancers + uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=False) # nosec B104 diff --git a/src/api/auth.py b/src/api/auth.py index 1a64653..5286651 100644 --- a/src/api/auth.py +++ b/src/api/auth.py @@ -7,8 +7,6 @@ from botocore.exceptions import ClientError from fastapi import Depends, HTTPException, status from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from api.setting import DEFAULT_API_KEYS - api_key_param = os.environ.get("API_KEY_PARAM_NAME") api_key_secret_arn = os.environ.get("API_KEY_SECRET_ARN") api_key_env = os.environ.get("API_KEY") @@ -31,8 +29,9 @@ elif api_key_secret_arn: elif api_key_env: api_key = api_key_env else: - # For local use only. - api_key = DEFAULT_API_KEYS + raise RuntimeError( + "API Key is not configured. Please set up your API Key." + ) security = HTTPBearer() diff --git a/src/api/models/bedrock.py b/src/api/models/bedrock.py index 8bbc1ba..fba048b 100644 --- a/src/api/models/bedrock.py +++ b/src/api/models/bedrock.py @@ -310,7 +310,8 @@ class BedrockModel(BaseChatModel): if message.role != "system": # ignore system messages here continue - assert isinstance(message.content, str) + if not isinstance(message.content, str): + raise TypeError(f"System message content must be a string, got {type(message.content).__name__}") system_prompts.append({"text": message.content}) return system_prompts @@ -580,7 +581,8 @@ class BedrockModel(BaseChatModel): tool_config["toolChoice"] = {"auto": {}} else: # Specific tool to use - assert "function" in chat_request.tool_choice + if "function" not in chat_request.tool_choice: + raise ValueError("tool_choice must contain 'function' key when specifying a specific tool") tool_config["toolChoice"] = {"tool": {"name": chat_request.tool_choice["function"].get("name", "")}} args["toolConfig"] = tool_config # add Additional fields to enable extend thinking @@ -784,7 +786,7 @@ class BedrockModel(BaseChatModel): return base64.b64decode(image_data), content_type.group(1) # Send a request to the image URL - response = requests.get(image_url) + response = requests.get(image_url, timeout=30) # Check if the request was successful if response.status_code == 200: content_type = response.headers.get("Content-Type") diff --git a/src/api/setting.py b/src/api/setting.py index 4e0a7bb..43fd2b7 100644 --- a/src/api/setting.py +++ b/src/api/setting.py @@ -1,7 +1,5 @@ import os -DEFAULT_API_KEYS = "bedrock" - API_ROUTE_PREFIX = os.environ.get("API_ROUTE_PREFIX", "/api/v1") TITLE = "Amazon Bedrock Proxy APIs"