Chapter 9: Security Configuration

Overview

Security in WSO2 spans transport layer encryption, authentication, authorization, message-level protection, and threat mitigation. This chapter covers practical configuration for each layer.

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.