Chapter 9: Security Configuration
This chapter walks through the security layers in WSO2, from TLS at the edge to scope-based authorization deep in the API, and the configuration that makes each one real.
Overview
WSO2 security stacks across transport encryption, authentication, authorization, message protection, and threat mitigation. Each layer has its own configuration shape, and skipping any of them tends to show up later as an incident.
Security Layers
┌─────────────────────────────────────────┐
│ 1. Network Security (Firewall, WAF) │
├─────────────────────────────────────────┤
│ 2. Transport Security (TLS/mTLS) │
├─────────────────────────────────────────┤
│ 3. Authentication (OAuth2, JWT, SAML) │
├─────────────────────────────────────────┤
│ 4. Authorization (Scopes, RBAC, XACML) │
├─────────────────────────────────────────┤
│ 5. Message Security (Encryption, Sign) │
├─────────────────────────────────────────┤
│ 6. Threat Protection (Rate limit, WAF) │
└─────────────────────────────────────────┘
Transport Security (TLS)
Keystore Configuration
WSO2 uses Java Keystores (JKS) for TLS certificates.
Default Keystores:
| Keystore | File | Purpose |
|---|---|---|
| Primary | wso2carbon.jks | Server identity (TLS) |
| Truststore | client-truststore.jks | Trusted CA certificates |
| Internal | internal.jks | Internal encryption |
Creating a Production Keystore:
# Generate key pair
keytool -genkey -alias apim.example.com \
-keyalg RSA -keysize 2048 \
-keystore apim.jks -storepass changeit \
-dname "CN=apim.example.com,O=MyOrg,L=London,C=UK" \
-validity 365
# Generate CSR for CA signing
keytool -certreq -alias apim.example.com \
-keystore apim.jks -storepass changeit \
-file apim.csr
# Import CA-signed certificate
keytool -import -alias apim.example.com \
-keystore apim.jks -storepass changeit \
-file apim-signed.crt
# Import to truststore
keytool -import -alias apim.example.com \
-keystore client-truststore.jks -storepass wso2carbon \
-file apim-signed.crt
deployment.toml:
[keystore.primary]
file_name = "apim.jks"
password = "changeit"
alias = "apim.example.com"
key_password = "changeit"
[truststore]
file_name = "client-truststore.jks"
password = "wso2carbon"
[keystore.internal]
file_name = "internal.jks"
password = "changeit"
alias = "internal"
key_password = "changeit"
Mutual TLS (mTLS)
Client and server both verify each other's certificates.
# Enable mTLS on gateway
[transport.https.sslHostConfig.properties]
certificateVerification = "required"
# Options: "required", "optional", "none"
[transport.https.sslHostConfig.certificate]
certificate_key_file = "/path/to/server.key"
certificate_file = "/path/to/server.crt"
certificate_chain_file = "/path/to/ca-chain.crt"
Import client certificate to truststore:
keytool -import -alias client-app \
-keystore client-truststore.jks \
-storepass wso2carbon \
-file client-certificate.crt
TLS Version and Cipher Configuration
# Disable weak protocols
[transport.https.sslHostConfig.properties]
protocols = "+TLSv1.2,+TLSv1.3"
# Restrict cipher suites
[transport.https.sslHostConfig]
ciphers = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384"
OAuth 2.0 Security
Token Types
| Token Type | Description | Recommended For |
|---|---|---|
| Opaque | Random string, validated by Key Manager | Internal APIs |
| JWT | Self-contained, verifiable without calling IS | Distributed systems, microservices |
JWT Token Configuration
# Enable JWT tokens
[apim.jwt]
enable = true
encoding = "base64"
claim_dialect = "http://wso2.org/claims"
header = "X-JWT-Assertion"
signing_algorithm = "SHA256withRSA"
[apim.jwt.token_generation]
impl = "org.wso2.carbon.apimgt.keymgt.token.JWTGenerator"
JWT Token Structure:
{
"header": {
"alg": "RS256",
"typ": "JWT",
"kid": "gateway_certificate_alias"
},
"payload": {
"sub": "admin@carbon.super",
"aud": "http://org.wso2.apimgt/gateway",
"iss": "https://apim.example.com/oauth2/token",
"exp": 1739500800,
"iat": 1739497200,
"jti": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"scope": "read_data write_data",
"application": {
"id": 1,
"name": "MyApp",
"tier": "Unlimited",
"owner": "admin"
}
}
}
Backend JWT Forwarding
Pass a signed JWT to backend services so they can verify the caller without calling back to the gateway.
[apim.jwt]
enable = true
header = "X-JWT-Assertion"
# Backend can validate JWT using gateway's public key
Backend verification (Java example):
String jwtHeader = request.getHeader("X-JWT-Assertion");
DecodedJWT jwt = JWT.decode(jwtHeader);
// Verify signature with gateway's public key
RSAPublicKey publicKey = getGatewayPublicKey();
Algorithm algorithm = Algorithm.RSA256(publicKey, null);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("https://apim.example.com/oauth2/token")
.build();
verifier.verify(jwtHeader);
String user = jwt.getSubject();
String scopes = jwt.getClaim("scope").asString();
API Key Security
Simpler alternative to OAuth for low-risk APIs.
Enabling API Keys
Publisher Portal → Select API → Runtime Configuration
→ Application Level Security → API Key → Enable
Using API Keys:
# Via header
curl -H "apikey: eyJ4NXQiOiJNell4TW1Ga09..." \
https://gateway:8243/employees/1.0.0/employees
# Via query parameter
curl "https://gateway:8243/employees/1.0.0/employees?apikey=eyJ4NXQiOiJNell4TW1Ga09..."
API Key vs OAuth 2.0
| Feature | API Key | OAuth 2.0 |
|---|---|---|
| Complexity | Simple | Moderate |
| Token refresh | No (long-lived) | Yes (refresh token) |
| Scopes | No | Yes |
| User context | No | Yes |
| Revocation | Manual | API call |
| Best for | Internal, low-risk | External, sensitive |
Scope-Based Authorization
Defining Scopes
# In API definition
x-wso2-security:
scopes:
- name: employee:read
description: Read employee data
roles: hr_viewer, hr_admin
- name: employee:write
description: Create/update employees
roles: hr_admin
- name: employee:delete
description: Delete employees
roles: hr_admin
paths:
/employees:
get:
x-wso2-scopes: [employee:read]
post:
x-wso2-scopes: [employee:write]
/employees/{id}:
delete:
x-wso2-scopes: [employee:delete]
Requesting Scoped Tokens
# Request token with specific scopes
curl -X POST https://localhost:9443/oauth2/token \
-H "Authorization: Basic <base64(key:secret)>" \
-d "grant_type=client_credentials&scope=employee:read employee:write"
# Response includes granted scopes
{
"access_token": "...",
"scope": "employee:read employee:write",
"token_type": "Bearer",
"expires_in": 3600
}
CORS Configuration
[apim.cors]
enable = true
allow_origins = ["https://myapp.com", "https://admin.myapp.com"]
allow_methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]
allow_headers = ["Authorization", "Content-Type", "X-Requested-With"]
allow_credentials = true
max_age = 3600
Per-API CORS (in Publisher):
API → Runtime Configuration → CORS Configuration
Allow Origins: https://myapp.com
Allow Methods: GET, POST
Allow Headers: Authorization, Content-Type
Threat Protection
Regular Expression Threat Protection
Block injection attacks in headers, query params, and body.
<!-- Threat protection policy -->
<threatProtection>
<header>
<pattern>.*[<>].*</pattern> <!-- Block angle brackets in headers -->
</header>
<queryParam>
<pattern>.*[;'"\\].*</pattern> <!-- Block SQL injection chars -->
</queryParam>
</threatProtection>
JSON Threat Protection
Limit JSON payload characteristics to prevent DoS.
[apim.threat_protection.json]
max_property_count = 100
max_string_length = 10000
max_array_element_count = 500
max_depth = 10
max_key_length = 100
XML Threat Protection
[apim.threat_protection.xml]
dtd_enabled = false
external_entities_enabled = false
max_depth = 30
max_element_count = 10000
max_attribute_count = 50
max_attribute_length = 1024
entity_expansion_limit = 100
max_children_per_element = 500
Rate Limiting as Security
# Global rate limits
[apim.throttling]
enable_data_publishing = true
enable_policy_deploy = true
# IP-based blocking
[apim.throttling.blacklist_condition]
enabled = true
# Burst control
[apim.throttling.properties]
enable_header_based_throttling = true
enable_jwt_claim_based_throttling = true
enable_query_param_based_throttling = true
Custom Rate Limit Policy (per-resource):
Publisher → API → Resources → GET /employees
Throttling Policy: Custom
Request Count: 100
Time Unit: minute
Condition Groups:
- Header "X-Client-Type" = "mobile" → 50 per minute
- IP Range 10.0.0.0/8 → 500 per minute
Security for Micro Integrator
Securing REST APIs
<api name="SecuredAPI" context="/secured" xmlns="http://ws.apache.org/ns/synapse">
<resource methods="GET" uri-template="/data">
<inSequence>
<!-- Validate API key from header -->
<property name="apiKey" expression="get-property('transport', 'X-API-Key')"/>
<filter xpath="$ctx:apiKey != 'expected-secret-key'">
<then>
<payloadFactory media-type="json">
<format>{"error": "Unauthorized"}</format>
</payloadFactory>
<property name="HTTP_SC" value="401" scope="axis2"/>
<respond/>
</then>
</filter>
<!-- Process authenticated request -->
<call>
<endpoint key="BackendEndpoint"/>
</call>
<respond/>
</inSequence>
</resource>
</api>
Securing with JWT Validation
<api name="JWTSecuredAPI" context="/jwt-secured" xmlns="http://ws.apache.org/ns/synapse">
<resource methods="GET" uri-template="/data">
<inSequence>
<property name="authHeader" expression="get-property('transport', 'Authorization')"/>
<!-- Extract JWT token -->
<script language="js">
<![CDATA[
var auth = mc.getProperty('authHeader');
if (auth && auth.startsWith('Bearer ')) {
mc.setProperty('jwtToken', auth.substring(7));
} else {
mc.setProperty('jwtToken', '');
}
]]>
</script>
<!-- Validate JWT -->
<class name="com.example.JWTValidatorMediator"/>
<filter xpath="$ctx:jwtValid != 'true'">
<then>
<payloadFactory media-type="json">
<format>{"error": "Invalid or expired token"}</format>
</payloadFactory>
<property name="HTTP_SC" value="401" scope="axis2"/>
<respond/>
</then>
</filter>
<call>
<endpoint key="BackendEndpoint"/>
</call>
<respond/>
</inSequence>
</resource>
</api>
Security Best Practices Checklist
Production Hardening
| Task | Configuration |
|---|---|
| Change default passwords | super_admin.username, super_admin.password |
| Replace default keystore | Generate proper certs, never use wso2carbon.jks in production |
| Disable HTTP transport | Remove port 8280, force HTTPS only |
| Enable TLS 1.2+ only | Disable SSLv3, TLS 1.0, TLS 1.1 |
| Restrict management console | Bind to internal network IP only |
| Enable audit logging | [audit_log] enable = true |
| Disable unused features | Remove unnecessary Carbon features |
| Set password policies | Min length, complexity, expiry |
Disable HTTP (Force HTTPS)
# Remove or comment out HTTP listener
# [[transport.http]]
# listener.port = 8280
# Only keep HTTPS
[[transport.https]]
listener.port = 8243
Restrict Management Console Access
[admin_console]
enable = true
[transport.https.properties]
# Bind to internal IP only
bind_address = "10.0.1.5"
Password Policies
[identity_mgt.password_policy]
min_length = 12
max_length = 64
pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{12,}$"
error_msg = "Password must be 12+ chars with upper, lower, digit, and special character"
[identity_mgt.account_locking]
enable = true
max_failed_attempts = 5
auto_unlock_time_in_minutes = 30
Security Headers
<!-- Add security headers to all responses -->
<sequence name="securityHeaders" xmlns="http://ws.apache.org/ns/synapse">
<property name="Strict-Transport-Security"
value="max-age=31536000; includeSubDomains"
scope="transport"/>
<property name="X-Content-Type-Options"
value="nosniff"
scope="transport"/>
<property name="X-Frame-Options"
value="DENY"
scope="transport"/>
<property name="Content-Security-Policy"
value="default-src 'self'"
scope="transport"/>
<property name="X-XSS-Protection"
value="1; mode=block"
scope="transport"/>
</sequence>
Key Takeaways
- Never use default keystores or credentials in production
- TLS 1.2+ should be the minimum; disable all older protocols
- JWT tokens are preferred for distributed architectures; opaque tokens for simple setups
- Scope-based authorization maps API operations to fine-grained permissions
- JSON/XML threat protection guards against injection and DoS attacks
- Mutual TLS provides the strongest client authentication
- Security headers prevent common web vulnerabilities
- Rate limiting is both a performance and security control
Next Steps
Continue to Chapter 10: Deployment Patterns to learn about production deployment, clustering, and scaling.