This document covers the security architecture and best practices for Vesper.
Vesper implements defense-in-depth security across all layers:
┌─────────────────────────────────────────────────────────────┐
│ CloudFront │
│ (DDoS Protection) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ AWS WAF v2 │
│ (OWASP Rules, Rate Limiting, Bot Protection) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ Application Load Balancer │
│ (TLS Termination) │
└─────────────────────────┬───────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────┐
│ API Gateway │
│ (JWT Validation, Rate Limiting) │
└─────────────────────────────────────────────────────────────┘
Vesper uses AWS Cognito for identity management:
| Component | Purpose |
|---|---|
| User Pool | User directory and authentication |
| App Clients | OAuth2/OIDC for web apps |
| Custom Claims | Tenant ID for multi-tenancy |
# infrastructure/terraform/modules/auth/cognito.tf
resource "aws_cognito_user_pool" "vesper" {
name = "vesper-${var.environment}"
password_policy {
minimum_length = 12
require_lowercase = true
require_numbers = true
require_symbols = true
require_uppercase = true
}
mfa_configuration = "ON"
software_token_mfa_configuration {
enabled = true
}
}The API Gateway validates JWTs on every request:
# apps/api-gateway/app/middleware/auth.py
class JWTValidator:
"""Validates JWTs using JWKS from Cognito."""
async def validate_token(self, token: str) -> dict:
# Fetch and cache JWKS
jwks = await self._get_jwks()
# Decode and verify token
header = jwt.get_unverified_header(token)
key = self._find_key(jwks, header["kid"])
payload = jwt.decode(
token,
key,
algorithms=["RS256"],
issuer=self.issuer,
audience=self.client_id,
)
# Verify expiration
if payload["exp"] < time.time():
raise TokenExpiredError()
return payloadExpected JWT claims:
{
"sub": "user-uuid",
"iss": "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_xxx",
"aud": "client-id",
"exp": 1700000000,
"iat": 1699996400,
"email": "user@example.com",
"cognito:groups": ["analysts", "admins"],
"custom:tenant_id": "tenant-123"
}| Role | Permissions |
|---|---|
analysts |
Chat, view citations, view traces |
operators |
All analyst + trigger DAGs, view evals |
admins |
All operator + manage users, modify config |
Every request includes tenant context:
# Tenant ID extraction from JWT
tenant_id = claims.get("custom:tenant_id", "default")
# All queries are scoped to tenant
results = await vector_store.query(
query=query,
filter={"tenant_id": tenant_id},
top_k=10,
)| Rule Set | Description |
|---|---|
| AWSManagedRulesCommonRuleSet | OWASP Top 10 protections |
| AWSManagedRulesKnownBadInputsRuleSet | Known attack patterns |
| AWSManagedRulesSQLiRuleSet | SQL injection protection |
| AWSManagedRulesLinuxRuleSet | Linux-specific attacks |
| AWSManagedRulesBotControlRuleSet | Bot detection |
# infrastructure/terraform/modules/edge/waf.tf
rule {
name = "RateLimitRule"
priority = 1
action {
block {}
}
statement {
rate_based_statement {
limit = 2000 # requests per 5 minutes
aggregate_key_type = "IP"
}
}
}variable "allowed_countries" {
default = ["US", "CA", "GB", "DE"] # Restrict to specific countries
}# infrastructure/terraform/modules/edge/main.tf
resource "aws_acm_certificate" "main" {
domain_name = var.domain_name
validation_method = "DNS"
subject_alternative_names = [
"*.${var.domain_name}",
"api.${var.domain_name}",
"ops.${var.domain_name}"
]
}Using TLS 1.3 with strong ciphers:
resource "aws_lb_listener" "https" {
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
}All HTTP traffic is redirected to HTTPS:
resource "aws_lb_listener" "http_redirect" {
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}All secrets are stored encrypted in SSM:
# infrastructure/terraform/modules/ssm/main.tf
resource "aws_ssm_parameter" "openai_api_key" {
name = "/vesper/${var.environment}/openai-api-key"
type = "SecureString"
value = var.openai_api_key
key_id = aws_kms_key.ssm.arn
}Dedicated KMS key for secrets:
resource "aws_kms_key" "ssm" {
description = "KMS key for Vesper SSM parameters"
deletion_window_in_days = 7
enable_key_rotation = true
}ECS tasks retrieve secrets at runtime:
secrets = [
{
name = "OPENAI_API_KEY"
valueFrom = aws_ssm_parameter.openai_api_key.arn
},
{
name = "DATABASE_URL"
valueFrom = aws_ssm_parameter.database_url.arn
}
]┌─────────────────────────────────────────────────────────────┐
│ VPC │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Public Subnets │ │
│ │ ┌─────────┐ ┌─────────┐ │ │
│ │ │ ALB │ │ NAT │ │ │
│ │ └────┬────┘ └────┬────┘ │ │
│ └────────┼─────────────────────────────┼──────────────┘ │
│ │ │ │
│ ┌────────▼─────────────────────────────▼──────────────┐ │
│ │ Private Subnets │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ ECS │ │ RDS │ │ Redis │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
| Component | Inbound | Outbound |
|---|---|---|
| ALB | 443 from 0.0.0.0/0 | All to VPC |
| ECS Tasks | 8000 from ALB SG | 443 to 0.0.0.0/0 |
| RDS | 5432 from ECS SG | None |
| Redis | 6379 from ECS SG | None |
VPC endpoints for AWS services:
resource "aws_vpc_endpoint" "ssm" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.${var.region}.ssm"
vpc_endpoint_type = "Interface"
}All inputs are validated with Pydantic:
class ChatRequest(BaseModel):
query: str = Field(..., min_length=1, max_length=4096)
tenant_id: str = Field(..., pattern=r"^[a-zA-Z0-9-]+$")
stream: bool = True
@field_validator("query")
@classmethod
def validate_query(cls, v: str) -> str:
# Sanitize input
v = v.strip()
if not v:
raise ValueError("Query cannot be empty")
return vContent moderation for inputs and outputs:
class GuardrailsMiddleware:
async def check_input(self, query: str) -> bool:
# Check for PII
if self._contains_pii(query):
raise ContentBlockedError("PII detected in input")
# Check for prompt injection
if self._is_prompt_injection(query):
raise ContentBlockedError("Potential prompt injection")
return TrueAll requests are logged with security context:
logger.info(
"Request processed",
extra={
"request_id": request_id,
"tenant_id": tenant_id,
"user_id": user_id,
"path": request.url.path,
"method": request.method,
"status_code": response.status_code,
"latency_ms": latency_ms,
"client_ip": client_ip,
}
)Security events are logged separately:
security_logger.warning(
"Authentication failure",
extra={
"event_type": "auth_failure",
"reason": "invalid_token",
"client_ip": client_ip,
"user_agent": user_agent,
}
)Logs are shipped to CloudWatch with retention:
resource "aws_cloudwatch_log_group" "api" {
name = "/vesper/api-gateway"
retention_in_days = 90
}# .github/workflows/ci.yml
- name: Run Snyk security scan
uses: snyk/actions/python@master
with:
args: --severity-threshold=high- name: Scan Docker image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}
severity: "CRITICAL,HIGH"
exit-code: "1"- name: Run Bandit security linter
run: |
pip install bandit
bandit -r apps/ -ll -ii- All secrets in SSM Parameter Store
- MFA enabled for Cognito users
- WAF rules in blocking mode
- TLS 1.3 enforced
- Security groups least-privilege
- VPC flow logs enabled
- CloudTrail enabled
- Dependency vulnerabilities addressed
- Regular secret rotation
- Security patches applied
- Penetration testing (annual)
- Access reviews (quarterly)
- Incident response drills
- Log review automation
| Role | Contact |
|---|---|
| Security Lead | security@vesper.example.com |
| On-Call | PagerDuty |
| AWS TAM | Account team |
- Detection: Monitor for security events
- Containment: Isolate affected resources
- Analysis: Determine scope and impact
- Remediation: Fix vulnerability
- Recovery: Restore services
- Lessons Learned: Update procedures
# Block an IP immediately
aws wafv2 update-ip-set \
--name vesper-blocked-ips \
--scope REGIONAL \
--addresses "1.2.3.4/32"
# Rotate compromised secret
aws ssm put-parameter \
--name "/vesper/prod/openai-api-key" \
--value "new-key" \
--overwrite