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:

KeystoreFilePurpose
Primarywso2carbon.jksServer identity (TLS)
Truststoreclient-truststore.jksTrusted CA certificates
Internalinternal.jksInternal 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 TypeDescriptionRecommended For
OpaqueRandom string, validated by Key ManagerInternal APIs
JWTSelf-contained, verifiable without calling ISDistributed 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

FeatureAPI KeyOAuth 2.0
ComplexitySimpleModerate
Token refreshNo (long-lived)Yes (refresh token)
ScopesNoYes
User contextNoYes
RevocationManualAPI call
Best forInternal, low-riskExternal, 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>.*[&lt;&gt;].*</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

TaskConfiguration
Change default passwordssuper_admin.username, super_admin.password
Replace default keystoreGenerate proper certs, never use wso2carbon.jks in production
Disable HTTP transportRemove port 8280, force HTTPS only
Enable TLS 1.2+ onlyDisable SSLv3, TLS 1.0, TLS 1.1
Restrict management consoleBind to internal network IP only
Enable audit logging[audit_log] enable = true
Disable unused featuresRemove unnecessary Carbon features
Set password policiesMin 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.