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
timeToLivewindow (default 300 seconds) — if clock skew between client and server exceeds this, every request fails withwsse:MessageExpiredas 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:
UsernameTokenpassword digest mismatches return SOAP faults with HTTP 200 — standard uptime monitors see healthy responses - STR reference resolution failures: when a
wsse:SecurityTokenReferencecannot 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
- Log in to vigilmon.online and go to Monitors → New Monitor
- Choose HTTP / HTTPS
- Set the URL to your WSS4J health endpoint:
https://your-app.example.com/api/health/wss4j - Set the check interval to 1 minute
- Under Expected response, configure:
- Status code:
200 - Response body contains:
"status":"ok" - Response time threshold:
5000ms
- Status code:
- Under Alert channels, assign your Slack or PagerDuty channel
- 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
- In Vigilmon, go to Monitors → New Monitor → Heartbeat
- Set the name:
secure-service-wss4j-processing - Set the expected interval: 10 minutes
- Set the grace period: 15 minutes
- 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
2000msfor the health endpoint (slow keystore access may indicate I/O or HSM latency) - Alert at
3000msfor the secure service WSDL (slow WSDL delivery blocks client initialisation) - Alert at
8000msfor 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.