tutorial

How to Monitor Apache Santuario XML Security Health with Vigilmon

Apache Santuario XML signature verification failures silently accept forged documents, key store rotation errors break XML encryption for the entire application, and CRL endpoint timeouts block every inbound signed request. Learn how to monitor Santuario XML security service availability, key store health, and signing pipeline liveness with Vigilmon HTTP probes and heartbeat monitors.

Apache Santuario is the reference Java implementation of XML Digital Signature (XMLDSig) and XML Encryption (XMLEnc) standards, used in SAML authentication stacks, WS-Security enterprise integrations, and XML-based document signing workflows. Santuario wraps the Java Cryptography Architecture around a DOM-based XML canonicalization and signature engine — when a Santuario-based SAML service provider upgrades its JDK and the new JDK ships a different default XML canonicalization algorithm, all inbound SAML assertions silently fail signature verification and users lose SSO access while the IdP continues to issue correctly-signed assertions; when the application's key store file is accidentally truncated during a rotation script, Santuario throws KeyStoreException: keystore password was incorrect at class-load time and the entire application fails to start, taking down all services that depend on it; when the CRL distribution point referenced in the signing certificate is unreachable due to a firewall change, every Santuario signature verification call blocks for the configured timeout duration — at 30 seconds per call, a modest inbound SAML traffic spike serializes into a queue that exhausts server threads in seconds. These are authentication and security infrastructure failures disguised as application startup errors or user-visible login failures.

Vigilmon gives you external visibility into Santuario's XML security layer through HTTP probe monitoring and heartbeat monitors. This tutorial covers both.


Why Apache Santuario Needs External Monitoring

Santuario's failure modes are subtle and high-impact:

  • Canonicalization algorithm mismatch after JDK upgrade: XML signature verification depends on the exact canonicalization algorithm applied to the signed XML; a JDK upgrade that changes the default transform provider causes all existing signatures to fail verification with XMLSignatureException — the application accepts no signed documents while signing still works, creating an asymmetric failure
  • Key store corruption or rotation failure: Santuario loads signing and verification keys from a JKS or PKCS12 key store at startup; a failed key rotation that leaves a truncated key store file causes all Santuario operations to fail at class initialization, taking down the entire application
  • CRL endpoint timeout cascade: Santuario's certificate revocation checking follows the CRL Distribution Points extension in signing certificates; an unreachable CRL endpoint causes every verification call to block for the full socket timeout before returning, serializing all signature verification operations on a single thread queue
  • OCSP responder failure during signature validation: applications configured to use OCSP for certificate status instead of CRL block Santuario's verification pipeline when the OCSP responder returns 5xx or times out — all inbound signed documents are rejected with CertPathValidatorException until the OCSP responder recovers
  • XML namespace prefix normalization bug after upgrade: Santuario versions before 3.0.3 have known bugs with namespace prefix normalization in certain XML document structures; an upgrade that introduces or exposes the bug causes Santuario to produce invalid signatures for documents with specific namespace patterns — signatures verify locally but fail when sent to external validators
  • Thread-unsafe SignatureFactory access under concurrency: some Santuario integration patterns share a static XMLSignatureFactory instance; under concurrent signature creation requests, the shared factory's internal DOM state is corrupted, causing intermittent NullPointerException in the signing path with no clear error message

External monitoring with Vigilmon adds:

  • Proactive alerting when Santuario-dependent signing or verification endpoints fail or slow down
  • Key store health monitoring independent of application startup state
  • Heartbeat monitoring so you know when scheduled batch document signing and certificate rotation jobs complete successfully
  • Multi-region probe consensus that filters transient CRL endpoint timeouts from genuine Santuario signing pipeline failures

Step 1: Build a Santuario XML Security Health Endpoint

Santuario does not ship a standalone health endpoint. Build one that exercises the sign-and-verify round trip using a test key pair.

Java Santuario Health Servlet

// health/SantuarioHealthServlet.java
package com.yourorg.santuario.health;

import org.apache.xml.security.Init;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.w3c.dom.*;
import javax.servlet.http.*;
import javax.xml.parsers.*;
import java.io.*;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.*;

public class SantuarioHealthServlet extends HttpServlet {

    private KeyPair probeKeyPair;
    private X509Certificate probeCert;

    @Override
    public void init() throws javax.servlet.ServletException {
        Init.init();
        try {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
            kpg.initialize(2048);
            probeKeyPair = kpg.generateKeyPair();
        } catch (Exception e) {
            throw new javax.servlet.ServletException("Failed to initialize Santuario probe", e);
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        Map<String, String> checks = new LinkedHashMap<>();
        boolean anyFailed = false;

        // Test sign-then-verify round trip
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().newDocument();

            Element root = doc.createElement("probe");
            root.setAttributeNS(null, "Id", "probe-element");
            doc.appendChild(root);

            XMLSignature sig = new XMLSignature(doc, "", XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);
            root.appendChild(sig.getElement());

            Transforms transforms = new Transforms(doc);
            transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
            sig.addDocument("#probe-element", transforms, Constants.ALGO_ID_DIGEST_SHA256);

            sig.sign(probeKeyPair.getPrivate());

            boolean valid = sig.checkSignatureValue(probeKeyPair.getPublic());
            if (valid) {
                checks.put("sign_verify_roundtrip", "ok");
            } else {
                checks.put("sign_verify_roundtrip", "failed:invalid_signature");
                anyFailed = true;
            }
        } catch (Exception e) {
            checks.put("sign_verify_roundtrip", "down:" + e.getClass().getSimpleName());
            anyFailed = true;
        }

        // Test key store accessibility
        String keystorePath = System.getenv("SANTUARIO_KEYSTORE_PATH");
        if (keystorePath != null) {
            try {
                KeyStore ks = KeyStore.getInstance("JKS");
                try (InputStream fis = new FileInputStream(keystorePath)) {
                    ks.load(fis, System.getenv("SANTUARIO_KEYSTORE_PASS").toCharArray());
                }
                int keyCount = Collections.list(ks.aliases()).size();
                checks.put("keystore", "ok:" + keyCount + "_entries");
            } catch (Exception e) {
                checks.put("keystore", "down:" + e.getClass().getSimpleName());
                anyFailed = true;
            }
        }

        resp.setContentType("application/json");
        resp.setStatus(anyFailed ? 503 : 200);

        PrintWriter out = resp.getWriter();
        out.write("{\"status\":\"" + (anyFailed ? "degraded" : "ok") + "\",\"checks\":{");
        boolean first = true;
        for (Map.Entry<String, String> e : checks.entrySet()) {
            if (!first) out.write(",");
            out.write("\"" + e.getKey() + "\":\"" + e.getValue() + "\"");
            first = false;
        }
        out.write("}}");
    }
}

Node.js Sidecar

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

const app = express();
const APP_HOST = process.env.APP_HOST || 'localhost';
const APP_PORT = parseInt(process.env.APP_PORT || '8080');
const HEALTH_PATH = process.env.SANTUARIO_HEALTH_PATH || '/health/xml-security';
const APP_USER = process.env.APP_USER || '';
const APP_PASS = process.env.APP_PASS || '';

function probeSantuario() {
  return new Promise((resolve, reject) => {
    const headers = {};
    if (APP_USER) {
      headers['Authorization'] = 'Basic ' + Buffer.from(`${APP_USER}:${APP_PASS}`).toString('base64');
    }
    const req = http.get(
      {
        host: APP_HOST,
        port: APP_PORT,
        path: HEALTH_PATH,
        timeout: 8000,
        headers,
      },
      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/santuario', async (req, res) => {
  try {
    const result = await probeSantuario();
    const up = result.status === 200 &&
      (result.body.includes('"status":"ok"') || result.body.includes('"status": "ok"'));

    if (!up) {
      return res.status(503).json({
        status: 'down',
        upstream_status: result.status,
        body: result.body.substring(0, 500),
      });
    }

    return res.status(200).json({ status: 'ok' });
  } catch (err) {
    return res.status(503).json({ status: 'down', error: err.message });
  }
});

app.listen(3022);

Python Health Sidecar

# health_santuario.py
import os, subprocess, requests
from flask import Flask, jsonify

app = Flask(__name__)

APP_BASE = f"http://{os.environ.get('APP_HOST', 'localhost')}:{os.environ.get('APP_PORT', '8080')}"
HEALTH_PATH = os.environ.get('SANTUARIO_HEALTH_PATH', '/health/xml-security')
KEYSTORE_PATH = os.environ.get('SANTUARIO_KEYSTORE_PATH', '')
KEYSTORE_PASS = os.environ.get('SANTUARIO_KEYSTORE_PASS', '')

@app.route('/health/santuario')
def health():
    checks = {}
    any_failed = False

    # Probe the application's XML security health endpoint
    try:
        resp = requests.get(f'{APP_BASE}{HEALTH_PATH}', timeout=8)
        if resp.status_code != 200:
            checks['xml_security_endpoint'] = f'down:{resp.status_code}'
            any_failed = True
        else:
            data = resp.json()
            status = data.get('status', 'unknown')
            if status != 'ok':
                checks['xml_security_endpoint'] = f'degraded:{status}'
                any_failed = True
            else:
                checks['xml_security_endpoint'] = 'ok'
    except Exception as e:
        checks['xml_security_endpoint'] = f'down:{e}'
        any_failed = True

    # Probe key store file accessibility
    if KEYSTORE_PATH:
        try:
            result = subprocess.run(
                ['keytool', '-list', '-keystore', KEYSTORE_PATH,
                 '-storepass', KEYSTORE_PASS, '-noprompt'],
                capture_output=True, text=True, timeout=5
            )
            if result.returncode == 0:
                entry_count = result.stdout.count('Your keystore contains')
                checks['keystore'] = f'ok'
            else:
                checks['keystore'] = 'down:keytool_failed'
                any_failed = True
        except Exception as e:
            checks['keystore'] = f'down:{e}'
            any_failed = True

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

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

Step 2: Configure Vigilmon HTTP Monitor for Santuario

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

Add a second monitor for the application's SAML SSO entry point if Santuario backs your SAML stack:

  • URL: https://app.your-org.example.com/saml/login
  • Expected: status 200 or 302 (redirect to IdP)
  • Interval: 2 minutes
  • Alert channel: P1 pager — SAML endpoint failure means all SSO logins are blocked

For WS-Security deployments, add a monitor for the secured SOAP endpoint:

  • URL: https://app.your-org.example.com/ws/service?wsdl
  • Expected: status 200, body contains definitions
  • Interval: 5 minutes
  • Alert channel: P1 pager — WSDL endpoint failure indicates WS-Security stack is unavailable

Vigilmon's multi-region probe consensus filters transient CRL endpoint timeout events from genuine Santuario signing failures.


Step 3: Heartbeat Monitoring for Batch Signing and Certificate Rotation Jobs

Health endpoint monitoring catches sign-and-verify and key store failures, but not end-to-end batch document signing and scheduled certificate rotation problems. Santuario can respond to the health endpoint while a nightly batch signing job has silently failed because the signing certificate expired at midnight and the automated rotation job failed silently.

Vigilmon heartbeat monitors catch these: your batch signing and certificate management jobs ping Vigilmon after each successful execution.

Set Up Heartbeat Monitors

For batch document signing pipeline liveness:

  1. In Vigilmon, go to Monitors → New Monitor → Heartbeat
  2. Set the name: santuario-batch-signing
  3. Set the expected interval: 1 hour
  4. Set the grace period: 2 hours
  5. Save — copy the heartbeat URL

For certificate rotation job liveness:

  1. Create another Heartbeat monitor named santuario-cert-rotation
  2. Set the expected interval: 24 hours
  3. Set the grace period: 26 hours
  4. Save — copy the heartbeat URL

Wire Heartbeats to Signing and Rotation Jobs

Add heartbeat pings to your batch document signing driver:

// signing/BatchDocumentSigner.java
package com.yourorg.santuario.signing;

import org.apache.xml.security.Init;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.apache.xml.security.utils.Constants;
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.net.http.*;
import java.net.URI;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.List;

public class BatchDocumentSigner {

    private final HttpClient http = HttpClient.newHttpClient();
    private final String heartbeatUrl = System.getenv("VIGILMON_SIGNING_HEARTBEAT_URL");
    private final PrivateKey signingKey;
    private final X509Certificate signingCert;

    public BatchDocumentSigner(PrivateKey key, X509Certificate cert) {
        Init.init();
        this.signingKey = key;
        this.signingCert = cert;
    }

    public void signBatch(List<Document> documents) throws Exception {
        int signed = 0;
        for (Document doc : documents) {
            try {
                Element root = doc.getDocumentElement();
                String baseUri = root.getAttributeNS(null, "baseUri");

                XMLSignature sig = new XMLSignature(
                    doc, baseUri, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256
                );
                root.appendChild(sig.getElement());

                Transforms transforms = new Transforms(doc);
                transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);
                transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_WITH_COMMENTS);
                sig.addDocument("", transforms, Constants.ALGO_ID_DIGEST_SHA256);

                sig.addKeyInfo(signingCert);
                sig.sign(signingKey);
                signed++;
            } catch (Exception e) {
                System.err.println("Signing failed for document: " + e.getMessage());
            }
        }
        System.out.println("Signed " + signed + "/" + documents.size() + " documents");
        if (signed > 0) pingHeartbeat(heartbeatUrl);
    }

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

For certificate rotation automation scripts:

#!/bin/bash
# santuario-cert-rotate.sh — run nightly by cron
/opt/pki/bin/rotate-signing-cert.sh \
  --keystore "$SANTUARIO_KEYSTORE_PATH" \
  --alias "xml-signing" \
  --days 365

if [ $? -eq 0 ] && [ -n "$VIGILMON_CERT_HEARTBEAT_URL" ]; then
  curl -s "$VIGILMON_CERT_HEARTBEAT_URL" > /dev/null 2>&1 || true
fi

Step 4: Alert Routing for Santuario Failures

Santuario failures range from a single document signing operation failing to the entire XML security stack going offline and blocking all authenticated service-to-service communication. Route alerts by severity:

| Monitor | Alert Channel | Priority | |---|---|---| | HTTP: XML security health endpoint | Slack + PagerDuty | P1 | | HTTP: SAML SSO login endpoint | Slack + PagerDuty | P1 | | HTTP: WS-Security WSDL endpoint | Slack + PagerDuty | P1 | | Heartbeat: batch document signing | Slack + PagerDuty | P1 | | Heartbeat: certificate rotation | Slack + PagerDuty | P1 | | Heartbeat: CRL/OCSP validation probe | Slack | P2 |

Set response time thresholds as early warning signals:

  • Alert at 5000ms for the XML security health endpoint (slow sign-verify round trip indicates JVM GC pressure or CRL lookup delays)
  • Alert at 3000ms for the SAML login endpoint (slow SAML responses indicate XML canonicalization bottleneck or signature verification queue buildup)
  • Alert at 2 hours gap in batch signing heartbeat (stalled signing means legal or compliance documents are not being processed)
  • Alert at 26 hours gap in certificate rotation heartbeat (missed rotation means signing certificates may expire silently)

For production XML security deployments, set up a status page in Vigilmon showing XML signing service availability and certificate rotation liveness — this lets security operations teams identify Santuario failures before they surface as SAML login failures or WS-Security integration errors that block entire business workflows.


Summary

Santuario failures present as XML signature verification failures or silent CRL timeout cascades before they escalate to full authentication stack outages — SAML users see login errors and WS-Security integrations receive XMLSignatureException while the application server reports HTTP 200 because the web layer is up but the Santuario signing engine is stalled. External monitoring catches these before they become authentication incidents:

| Monitor Type | What It Covers | |---|---| | HTTP monitor on health endpoint | Sign-verify round trip and key store accessibility | | HTTP monitor on SAML login endpoint | SAML authentication stack availability | | HTTP monitor on WS-Security WSDL | WS-Security service layer availability | | Heartbeat monitor (batch signing) | End-to-end document signing pipeline success | | Heartbeat monitor (cert rotation) | Certificate rotation and key store update liveness |

Get started free at vigilmon.online — your first Santuario XML 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 →