Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 28 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ The service supports multi-region deployment to AWS Lambda across multiple geogr
- `AWS_ACCESS_KEY_ID` - AWS access key for deployment
- `AWS_SECRET_ACCESS_KEY` - AWS secret key for deployment
- `CF_SECRET_KEY` - CloudFlare Turnstile secret key (optional, for bot protection)
- `CORS_ORIGIN` - CORS origin URL (optional, defaults to `https://secrets.merabytes.com`)
- `CORS_ORIGIN` - Comma-separated list of allowed CORS origins (optional, defaults to secrets.merabytes.com, app.merabytes.com, and local.merabytes.com)

### Automated Deployment

Expand Down Expand Up @@ -587,31 +587,51 @@ aws lambda update-function-configuration \

## CORS Configuration

The Lambda function supports Cross-Origin Resource Sharing (CORS) to allow web applications to interact with the secrets API. The CORS origin URL can be configured through the `CORS_ORIGIN` environment variable.
The Lambda function supports Cross-Origin Resource Sharing (CORS) to allow web applications to interact with the secrets API. The service now supports **multiple allowed origins** with dynamic origin matching.

### Configuration

- **Default**: If not specified, defaults to `https://secrets.merabytes.com`
- **Custom**: Set `CORS_ORIGIN` environment variable to your frontend URL
- **Default Origins**: If not specified, the service allows requests from:
- `https://secrets.merabytes.com`
- `https://app.merabytes.com`
- `https://local.merabytes.com`

**Example:**
- **Custom Origins**: Set `CORS_ORIGIN` environment variable to a comma-separated list of allowed origins
- **Dynamic Matching**: The service inspects the `Origin` header in each request and returns it in the response if it's in the allowed list

**Example - Single origin:**
```bash
# Set custom CORS origin
# Set custom CORS origin (single domain)
aws lambda update-function-configuration \
--function-name AcidoSecrets \
--environment "Variables={...,CORS_ORIGIN=https://myapp.example.com}"
```

**Example - Multiple origins:**
```bash
# Set custom CORS origins (multiple domains, comma-separated)
aws lambda update-function-configuration \
--function-name AcidoSecrets \
--environment "Variables={...,CORS_ORIGIN=https://app1.example.com,https://app2.example.com,https://local.example.com}"
```

**In GitHub Actions:**
Add `CORS_ORIGIN` to your repository secrets to configure it during deployment.
Add `CORS_ORIGIN` to your repository secrets to configure it during deployment. Use a comma-separated list for multiple origins.

### CORS Headers

The service responds with the following CORS headers:
- `Access-Control-Allow-Origin`: Configurable via `CORS_ORIGIN` (default: `https://secrets.merabytes.com`)
- `Access-Control-Allow-Origin`: Dynamically set to match the request origin if it's in the allowed list (defaults to first allowed origin if no match)
- `Access-Control-Allow-Methods`: `POST, OPTIONS`
- `Access-Control-Allow-Headers`: `Content-Type`

### How It Works

1. The service extracts the `Origin` header from incoming requests
2. If the origin is in the allowed list, it's returned in the `Access-Control-Allow-Origin` response header
3. If the origin is not in the allowed list (or missing), the first allowed origin is used as the default
4. This allows multiple frontend applications to interact with the same Lambda function while maintaining security

## CloudFlare Turnstile Integration

CloudFlare Turnstile provides bot protection to prevent abuse of the secrets service. It's **optional** and only activated when the `CF_SECRET_KEY` environment variable is set.
Expand Down
85 changes: 82 additions & 3 deletions lambda_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,84 @@ def _decrypt_with_secret_key(data: str) -> str:
return decrypt_secret(data, secret_key)


# CORS headers - origin is configurable via CORS_ORIGIN environment variable
# CORS configuration
# Default allowed origins - supports multiple origins
DEFAULT_ALLOWED_ORIGINS = [
"https://secrets.merabytes.com",
"https://app.merabytes.com",
"https://local.merabytes.com"
]

# Allow custom origins via CORS_ORIGIN environment variable (comma-separated)
CORS_ORIGIN_ENV = os.environ.get("CORS_ORIGIN", "")
if CORS_ORIGIN_ENV:
# Parse comma-separated origins and filter out empty strings
ALLOWED_ORIGINS = [origin.strip() for origin in CORS_ORIGIN_ENV.split(",") if origin.strip()]
# If parsing resulted in an empty list, fall back to defaults
if not ALLOWED_ORIGINS:
ALLOWED_ORIGINS = DEFAULT_ALLOWED_ORIGINS
else:
ALLOWED_ORIGINS = DEFAULT_ALLOWED_ORIGINS

# Initialize CORS_HEADERS with default (will be updated per request)
CORS_HEADERS = {
"Access-Control-Allow-Origin": os.environ.get("CORS_ORIGIN", "https://secrets.merabytes.com"),
"Access-Control-Allow-Origin": ALLOWED_ORIGINS[0],
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Type": "application/json"
}


def _get_cors_headers(origin=None):
"""
Get CORS headers with appropriate origin.

If origin is provided and is in the allowed list, use it.
Otherwise, use the first allowed origin as default.

Args:
origin: Origin header from the request

Returns:
dict: CORS headers
"""
allowed_origin = ALLOWED_ORIGINS[0] # Default to first allowed origin

if origin and origin in ALLOWED_ORIGINS:
allowed_origin = origin

return {
"Access-Control-Allow-Origin": allowed_origin,
"Access-Control-Allow-Methods": "POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
"Content-Type": "application/json"
}


def _extract_origin(event):
"""
Extract the Origin header from the Lambda event.
Uses case-insensitive header lookup.

Args:
event: Lambda event object

Returns:
str: Origin header value or None
"""
# Try to get headers from the event
headers = event.get('headers', {})
if not headers:
return None

# HTTP header names are case-insensitive - search case-insensitively
for key, value in headers.items():
if key.lower() == 'origin':
return value

return None


def _get_version():
"""Read version from VERSION file."""
try:
Expand Down Expand Up @@ -542,7 +611,8 @@ def lambda_handler(event, context):
- SECRET_KEY: Additional encryption key for enhanced security (auto-generated during deployment)

Environment variables optional:
- CORS_ORIGIN: CORS origin URL (default: https://secrets.merabytes.com)
- CORS_ORIGIN: Comma-separated list of allowed CORS origins
(default: https://secrets.merabytes.com,https://app.merabytes.com,https://local.merabytes.com)

Multi-layer encryption:
- All secrets are encrypted with SECRET_KEY (system-level encryption, always applied)
Expand All @@ -564,7 +634,16 @@ def lambda_handler(event, context):
Returns:
dict: Response with statusCode and body containing operation result
"""
# Note: Lambda execution contexts are single-threaded and handle one request at a time,
# so modifying the global CORS_HEADERS here is safe and doesn't cause race conditions.
global CORS_HEADERS

original_event = event

# Extract origin from request and set CORS headers dynamically
origin = _extract_origin(original_event)
CORS_HEADERS = _get_cors_headers(origin)

event = parse_lambda_event(event)

# Handle OPTIONS preflight
Expand Down
Loading