This guide covers security considerations when implementing OpenCPX.
The /cpx endpoint should return references to evidence, not the evidence itself.
Don't do this:
{
"evidence": {
"iam_policies": [
{ "name": "admin", "permissions": [...] }
]
}
}Do this:
{
"evidence_refs": [
"https://evidence.company.com/iam-export.json"
]
}Presigned URLs should expire:
- Default: 1 hour
- Maximum: 24 hours
- Best practice: Generate fresh URLs per request
{
"evidence_refs": [{
"type": "presigned_url",
"url": "https://s3.aws.com/...?signature=...",
"expires": "2024-01-15T13:00:00Z"
}]
}- Only expose what's necessary
- Scope access to specific files, not buckets
- Use separate credentials for evidence access
For public compliance data (trust centers):
- No authentication required
- Expose only public information
- Rate limit to prevent abuse
// Public endpoint
mux.HandleFunc("/cpx", publicHandler)For sensitive or customer-specific data:
API Key:
func protectedHandler(w http.ResponseWriter, r *http.Request) {
apiKey := r.Header.Get("X-API-Key")
if !validateAPIKey(apiKey) {
http.Error(w, "Unauthorized", 401)
return
}
// Serve posture
}OAuth/JWT:
func protectedHandler(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
claims, err := validateJWT(token)
if err != nil {
http.Error(w, "Unauthorized", 401)
return
}
// Use claims to filter posture
}For multi-tenant applications:
func customerHandler(w http.ResponseWriter, r *http.Request) {
customerID := r.URL.Query().Get("customer_id")
// Verify customer has access
if !hasAccess(r.Context(), customerID) {
http.Error(w, "Forbidden", 403)
return
}
// Return customer-specific posture
posture := getPostureForCustomer(customerID)
}Use cloud provider presigned URLs:
AWS S3:
import boto3
from botocore.config import Config
s3 = boto3.client('s3', config=Config(signature_version='s3v4'))
url = s3.generate_presigned_url(
'get_object',
Params={
'Bucket': 'evidence-bucket',
'Key': 'soc2/report.pdf'
},
ExpiresIn=3600 # 1 hour
)Google Cloud Storage:
from google.cloud import storage
client = storage.Client()
bucket = client.bucket('evidence-bucket')
blob = bucket.blob('soc2/report.pdf')
url = blob.generate_signed_url(
version='v4',
expiration=timedelta(hours=1),
method='GET'
)Include hashes for integrity verification:
{
"evidence_refs": [{
"url": "https://...",
"hash": "sha256:a1b2c3d4e5f6...",
"size_bytes": 1048576
}]
}Consumers should verify:
import hashlib
def verify_evidence(content, expected_hash):
actual_hash = hashlib.sha256(content).hexdigest()
return f"sha256:{actual_hash}" == expected_hashLog all evidence access:
def log_evidence_access(request, evidence_ref):
logger.info({
"event": "evidence_access",
"user": request.user,
"evidence_url": evidence_ref["url"],
"timestamp": datetime.utcnow().isoformat(),
"ip_address": request.remote_addr,
"user_agent": request.headers.get("User-Agent")
})- Framework compliance status
- Overall scores
- Audit dates and auditors
- Certificate references
- Public policy documents
- Detailed control evidence
- Internal policies
- Customer-specific data
- Security configurations
- Vulnerability reports
- Credentials or secrets
- Customer PII
- Internal IP addresses
- Unredacted security findings
Prevent abuse with rate limiting:
import "golang.org/x/time/rate"
var limiter = rate.NewLimiter(rate.Every(time.Second), 10)
func rateLimitedHandler(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Too Many Requests", 429)
return
}
// Serve posture
}Validate all input:
def get_posture(request):
format = request.args.get('format', 'json')
if format not in ['json', 'yaml']:
abort(400, 'Invalid format')
customer_id = request.args.get('customer_id')
if customer_id and not customer_id.isalnum():
abort(400, 'Invalid customer_id')For endpoints like /cpx/{tenant}:
func handler(w http.ResponseWriter, r *http.Request) {
tenant := chi.URLParam(r, "tenant")
// Validate tenant ID format
if !isValidTenantID(tenant) {
http.Error(w, "Invalid tenant", 400)
return
}
// Check authorization
if !canAccessTenant(r.Context(), tenant) {
http.Error(w, "Forbidden", 403)
return
}
}Always use HTTPS:
- Enforce TLS 1.2+
- Use valid certificates
- Implement HSTS
- Redirect HTTP to HTTPS
func main() {
// Redirect HTTP to HTTPS
go http.ListenAndServe(":80", http.HandlerFunc(redirectToHTTPS))
// Serve HTTPS
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", handler)
}Configure CORS appropriately:
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Restrict to known domains
origin := r.Header.Get("Origin")
if isAllowedOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
w.Header().Set("Access-Control-Allow-Methods", "GET")
w.Header().Set("Access-Control-Allow-Headers", "Authorization")
next.ServeHTTP(w, r)
})
}Add security headers:
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'none'")
w.Header().Set("Cache-Control", "no-store")
next.ServeHTTP(w, r)
})
}Maintain an audit trail:
def get_posture(request):
# Log the request
audit_log.info({
"action": "cpx_endpoint_accessed",
"timestamp": datetime.utcnow().isoformat(),
"client_ip": request.remote_addr,
"user_agent": request.headers.get("User-Agent"),
"authenticated_user": getattr(request, "user", None),
"parameters": dict(request.args)
})
# Generate and return posture
return generate_posture()- Use HTTPS only
- Implement authentication for sensitive data
- Use presigned URLs for evidence
- Set short expiration times
- Include hash verification
- Validate all inputs
- Implement rate limiting
- Add security headers
- Log all access
- Never expose raw evidence in API
- Use time-limited access tokens
- Scope access to specific files
- Log evidence retrieval
- Verify integrity with hashes
- Monitor for unusual access patterns
- Alert on authentication failures
- Track evidence access
- Review logs regularly
If you find a security vulnerability in OpenCPX:
- Do not open a public issue
- Email security@opencpx.io
- Include detailed reproduction steps
- Allow time for a fix before disclosure
We follow responsible disclosure practices and will credit reporters.