tutorial

How to Monitor Apache WSS4J WS-Security Health with Vigilmon

Apache WSS4J secures SOAP messages through signing, encryption, and token validation — but expired timestamps, invalid signatures, and keystore misconfigurations fail silently with HTTP 200 SOAP faults. Learn how to monitor WSS4J security processor health with Vigilmon HTTP probes and heartbeat monitors.

Apache WSS4J is the reference Java implementation of the WS-Security specification. It signs and encrypts SOAP message bodies, validates wsse:UsernameToken and wsse:BinarySecurityToken assertions, and enforces timestamp freshness windows. WSS4J failure modes are particularly deceptive: an expired wsse:Timestamp causes every request to fail with a SOAP fault returned as HTTP 200, a misconfigured keystore silently rejects all signed messages, and a SAMLTokenValidator misconfiguration passes requests it should reject. The service endpoint responds, the HTTP check turns green, and the entire WS-Security layer is broken.

Vigilmon gives you external visibility into WSS4J security processor health through HTTP probe monitoring and heartbeat monitors that detect when valid messages stop being accepted. This tutorial covers both.


Why WSS4J Needs External Monitoring

WSS4J's cryptographic processing and token validation create failure modes that port checks and generic HTTP monitors miss:

  • Timestamp expiry: WSS4J enforces a timeToLive window (default 300 seconds) — if clock skew between client and server exceeds this, every request fails with wsse:MessageExpired as HTTP 200
  • Keystore misconfiguration: a missing or rotated certificate causes signature validation to fail for every incoming message without raising an HTTP error
  • Token validation failures: UsernameToken password digest mismatches return SOAP faults with HTTP 200 — standard uptime monitors see healthy responses
  • STR reference resolution failures: when a wsse:SecurityTokenReference cannot be resolved, WSS4J throws and the interceptor chain halts
  • CRL/OCSP unavailability: if certificate revocation checking is enabled and the CRL endpoint is unreachable, all signature validations fail or hang

External monitoring with Vigilmon adds:

  • Proactive alerting when the WSS4J health endpoint returns non-200 or unexpected bodies
  • Keystore and certificate validity monitoring via a custom health resource
  • Heartbeat monitoring for successful secure message processing
  • Multi-region availability checking from outside your network perimeter

Step 1: Build a WSS4J Health Endpoint

WSS4J does not expose an HTTP health interface — you need to add one that validates keystore availability, certificate expiry, and timestamp processor state.

Java JAX-RS Health Resource (WSS4J + Spring Boot)

// src/main/java/com/example/health/Wss4jHealthResource.java
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoFactory;
import org.apache.wss4j.dom.engine.WSSConfig;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.time.Instant;
import java.util.*;

@Path("/health")
public class Wss4jHealthResource {

    private final Properties cryptoProperties;

    public Wss4jHealthResource(Properties cryptoProperties) {
        this.cryptoProperties = cryptoProperties;
    }

    @GET
    @Path("/wss4j")
    @Produces(MediaType.APPLICATION_JSON)
    public Response health() {
        List<String> issues = new ArrayList<>();
        Map<String, Object> certInfo = new HashMap<>();

        // Verify WSSConfig initialises without exception
        try {
            WSSConfig.init();
        } catch (Exception e) {
            issues.add("wss_config_init_failed: " + e.getMessage());
        }

        // Verify the crypto/keystore is reachable and not empty
        try {
            Crypto crypto = CryptoFactory.getInstance(cryptoProperties);
            KeyStore ks = crypto.getKeyStore();
            if (ks == null) {
                issues.add("keystore_null");
            } else {
                List<Map<String, Object>> certs = new ArrayList<>();
                for (String alias : Collections.list(ks.aliases())) {
                    if (ks.isCertificateEntry(alias) || ks.isKeyEntry(alias)) {
                        X509Certificate cert = (X509Certificate) ks.getCertificate(alias);
                        if (cert != null) {
                            long daysUntilExpiry = (cert.getNotAfter().getTime() - System.currentTimeMillis()) / 86_400_000L;
                            Map<String, Object> entry = new HashMap<>();
                            entry.put("alias", alias);
                            entry.put("subject", cert.getSubjectX500Principal().getName());
                            entry.put("expires", cert.getNotAfter().toInstant().toString());
                            entry.put("days_remaining", daysUntilExpiry);
                            certs.add(entry);
                            if (daysUntilExpiry < 0) {
                                issues.add("cert_expired: " + alias);
                            } else if (daysUntilExpiry < 30) {
                                issues.add("cert_expiring_soon: " + alias + " (" + daysUntilExpiry + " days)");
                            }
                        }
                    }
                }
                certInfo.put("aliases", certs);
                certInfo.put("alias_count", certs.size());
            }
        } catch (Exception e) {
            issues.add("keystore_load_failed: " + e.getMessage());
        }

        if (!issues.isEmpty()) {
            return Response.status(503).entity(Map.of(
                "status", "degraded",
                "issues", issues,
                "certificates", certInfo
            )).build();
        }

        return Response.ok(Map.of(
            "status", "ok",
            "certificates", certInfo
        )).build();
    }
}

Register the health resource in your Spring Boot configuration:

// Wss4jConfig.java
@Configuration
public class Wss4jConfig {

    @Bean
    public Properties cryptoProperties() {
        Properties p = new Properties();
        p.setProperty("org.apache.wss4j.crypto.provider", "org.apache.wss4j.common.crypto.Merlin");
        p.setProperty("org.apache.wss4j.crypto.merlin.keystore.type", "JKS");
        p.setProperty("org.apache.wss4j.crypto.merlin.keystore.file", System.getenv("KEYSTORE_PATH"));
        p.setProperty("org.apache.wss4j.crypto.merlin.keystore.password", System.getenv("KEYSTORE_PASSWORD"));
        return p;
    }

    @Bean
    public Server jaxRsServer(Bus bus, Properties cryptoProperties) {
        JAXRSServerFactoryBean factory = new JAXRSServerFactoryBean();
        factory.setBus(bus);
        factory.setAddress("/api");
        factory.setServiceBeans(List.of(new Wss4jHealthResource(cryptoProperties)));
        return factory.create();
    }
}

Node.js Health Sidecar

// health/wss4j.js
const express = require('express');
const https = require('https');
const http = require('http');

const app = express();

const SERVICE_URL = process.env.WSS4J_SERVICE_URL || 'http://localhost:8080/services/SecureService';
const HEALTH_URL = process.env.WSS4J_HEALTH_URL || 'http://localhost:8080/api/health/wss4j';

function fetchUrl(url) {
  return new Promise((resolve, reject) => {
    const client = url.startsWith('https') ? https : http;
    const req = client.get(url, { timeout: 5000 }, res => {
      let body = '';
      res.on('data', chunk => { body += chunk; });
      res.on('end', () => resolve({ status: res.statusCode, body }));
    });
    req.on('error', reject);
    req.on('timeout', () => { req.destroy(); reject(new Error('timeout')); });
  });
}

app.get('/health/wss4j', async (req, res) => {
  const results = [];
  let allOk = true;

  // Proxy the Java health endpoint
  try {
    const { status, body } = await fetchUrl(HEALTH_URL);
    const parsed = JSON.parse(body);
    const ok = status === 200 && parsed.status === 'ok';
    results.push({ check: 'wss4j_processor', ok, status, detail: parsed });
    if (!ok) allOk = false;
  } catch (err) {
    results.push({ check: 'wss4j_processor', ok: false, error: err.message });
    allOk = false;
  }

  // Check that the secure service endpoint is reachable
  try {
    const { status } = await fetchUrl(SERVICE_URL);
    results.push({ check: 'service_endpoint', ok: status < 500, status });
    if (status >= 500) allOk = false;
  } catch (err) {
    results.push({ check: 'service_endpoint', ok: false, error: err.message });
    allOk = false;
  }

  return res.status(allOk ? 200 : 503).json({
    status: allOk ? 'ok' : 'degraded',
    checks: results,
  });
});

app.listen(3010);

Python Health Sidecar

# health_wss4j.py
import os, requests
from flask import Flask, jsonify

app = Flask(__name__)

SERVICE_URL = os.environ.get('WSS4J_SERVICE_URL', 'http://localhost:8080/services/SecureService')
HEALTH_URL = os.environ.get('WSS4J_HEALTH_URL', 'http://localhost:8080/api/health/wss4j')

@app.route('/health/wss4j')
def health():
    results = []
    all_ok = True

    try:
        r = requests.get(HEALTH_URL, timeout=5)
        parsed = r.json()
        ok = r.status_code == 200 and parsed.get('status') == 'ok'
        results.append({'check': 'wss4j_processor', 'ok': ok, 'status': r.status_code, 'detail': parsed})
        if not ok:
            all_ok = False
    except Exception as e:
        results.append({'check': 'wss4j_processor', 'ok': False, 'error': str(e)})
        all_ok = False

    try:
        r = requests.get(SERVICE_URL, timeout=5)
        ok = r.status_code < 500
        results.append({'check': 'service_endpoint', 'ok': ok, 'status': r.status_code})
        if not ok:
            all_ok = False
    except Exception as e:
        results.append({'check': 'service_endpoint', 'ok': False, 'error': str(e)})
        all_ok = False

    code = 200 if all_ok else 503
    return jsonify({'status': 'ok' if all_ok else 'degraded', 'checks': results}), code

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3010)

Step 2: Configure Vigilmon HTTP Monitor for WSS4J

  1. Log in to vigilmon.online and go to Monitors → New Monitor
  2. Choose HTTP / HTTPS
  3. Set the URL to your WSS4J health endpoint: https://your-app.example.com/api/health/wss4j
  4. Set the check interval to 1 minute
  5. Under Expected response, configure:
    • Status code: 200
    • Response body contains: "status":"ok"
    • Response time threshold: 5000ms
  6. Under Alert channels, assign your Slack or PagerDuty channel
  7. Save the monitor

Add a secondary monitor for the secure WSDL endpoint:

  • URL: https://your-app.example.com/services/SecureService?wsdl
  • Expected: 200, body contains: definitions
  • Interval: 2 minutes
  • Alert channel: P1 (WSDL unavailability means no client can initialise the stub)

Add a certificate expiry monitor by checking the health endpoint for days_remaining:

  • URL: https://your-app.example.com/api/health/wss4j
  • Expected body contains: "status":"ok"
  • Interval: 15 minutes (certificate state changes slowly)
  • Alert channel: P2 Slack (30-day warning), P1 PagerDuty (cert expired)

Vigilmon's multi-region probe consensus filters transient network issues from genuine security processor failures.


Step 3: Heartbeat Monitoring for WSS4J Secure Operations

Keystore availability monitoring catches configuration failures, but not functional ones. A WSS4J processor can load its keystore successfully but reject every signed message due to a key alias mismatch — the health endpoint returns 200, the interceptor chain rejects all traffic.

Vigilmon heartbeat monitors catch these: your service logic pings Vigilmon after each successfully validated and processed secure message.

Set Up the Heartbeat Monitor

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: secure-service-wss4j-processing
  3. Set the expected interval: 10 minutes
  4. Set the grace period: 15 minutes
  5. Save — copy the heartbeat URL, e.g. https://vigilmon.online/heartbeat/abc123xyz

Wire It Into a WSS4J-Secured JAX-WS Service

// SecureOrderServiceImpl.java
import javax.jws.WebService;
import java.net.http.*;
import java.net.URI;

@WebService(endpointInterface = "com.example.SecureOrderService")
public class SecureOrderServiceImpl implements SecureOrderService {

    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");

    @Override
    public OrderResponse submitOrder(OrderRequest request) {
        // WSS4J has already validated the signature and timestamp before this method is reached
        OrderResponse response = orderGateway.process(request);

        if (response.isAccepted()) {
            // Security validation passed and business logic succeeded
            pingHeartbeat();
        }

        return response;
    }

    private void pingHeartbeat() {
        if (heartbeatUrl == null) return;
        try {
            http.sendAsync(
                HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
            );
        } catch (Exception ignored) {}
    }
}

Wire It Into a WSS4J Validation Action for Cross-Cutting Pings

// VigilmonHeartbeatAction.java
import org.apache.wss4j.dom.action.Action;
import org.apache.wss4j.dom.handler.RequestData;
import org.apache.wss4j.dom.handler.WSHandler;
import org.apache.wss4j.dom.message.token.UsernameToken;
import org.w3c.dom.Document;
import java.net.http.*;
import java.net.URI;

public class VigilmonHeartbeatAction implements Action {

    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_HEARTBEAT_URL");

    @Override
    public void execute(WSHandler handler, int actionToDo, Document doc,
                        RequestData reqData) throws org.apache.wss4j.common.ext.WSSecurityException {
        // Called after WSS4J has successfully processed all security actions
        if (heartbeatUrl == null) return;
        try {
            http.sendAsync(
                HttpRequest.newBuilder(URI.create(heartbeatUrl)).GET().build(),
                HttpResponse.BodyHandlers.discarding()
            );
        } catch (Exception ignored) {}
    }
}

Step 4: Alert Routing for WSS4J Failures

WSS4J failures range from a single expired certificate (recoverable via key rotation) to a keystore load failure (all secure services unreachable). Route alerts by severity:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: /api/health/wss4j keystore + cert state | Slack + PagerDuty | P1 | | HTTP: Secure service WSDL availability | Slack + PagerDuty | P1 | | Heartbeat: successful secure message processing | Slack + PagerDuty | P1 | | HTTP: Certificate expiry warning (days_remaining < 30) | Slack | P2 |

Set response time thresholds as early warning signals:

  • Alert at 2000ms for the health endpoint (slow keystore access may indicate I/O or HSM latency)
  • Alert at 3000ms for the secure service WSDL (slow WSDL delivery blocks client initialisation)
  • Alert at 8000ms for actual service invocations (cryptographic operations are CPU-intensive; slow responses indicate saturation)

For services handling sensitive data, set up a status page in Vigilmon showing WSS4J processor health alongside certificate expiry state — operations teams can use this to pre-empt certificate rotation deadlines.


Summary

WSS4J failures are cryptographic-layer failures — the JVM is up, the port is open, but every secure message is being rejected by the security processor. External monitoring catches these before callers lose access:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on /api/health/wss4j | Keystore availability, certificate expiry, WSSConfig initialisation | | HTTP monitor on ?wsdl endpoint | Secure service WSDL availability for client stub generation | | Heartbeat monitor on secure message processing | End-to-end WSS4J validation and business logic liveness | | HTTP monitor on cert expiry check | Certificate days-remaining alert before expiry causes failures |

Get started free at vigilmon.online — your first WSS4J security monitor is running in under two minutes.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →