Skip to content

Instantly share code, notes, and snippets.

@joemiller
Created June 11, 2025 22:58
Show Gist options
  • Save joemiller/32dbe2ab82b1e47a0ade158b33f186dc to your computer and use it in GitHub Desktop.
Save joemiller/32dbe2ab82b1e47a0ade158b33f186dc to your computer and use it in GitHub Desktop.
Certificate Renewal Timeline visualizer (developed by Claude. Used to simulate CA + leaf renewal with cert-manager which does NOT clamp leaf expiration to CA expiration)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Certificate Renewal Timeline Visualizer</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 20px;
background: #f5f5f5;
}
.container {
max-width: 100%;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
color: #333;
margin-bottom: 10px;
}
.description {
color: #666;
margin-bottom: 30px;
line-height: 1.6;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
padding: 20px;
background: #f9f9f9;
border-radius: 8px;
}
.control-group {
display: flex;
flex-direction: column;
}
label {
font-weight: 600;
margin-bottom: 5px;
color: #555;
font-size: 14px;
}
input, select {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
input[type="number"] {
width: 100%;
}
button {
padding: 10px 20px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
font-weight: 600;
transition: background 0.2s;
}
button:hover {
background: #45a049;
}
.timeline-container {
position: relative;
margin: 20px 0;
min-height: 400px;
max-height: 800px;
overflow-x: auto;
overflow-y: auto;
border: 1px solid #e0e0e0;
border-radius: 8px;
background: #fafafa;
width: 100%;
}
.timeline {
position: relative;
min-width: 100%;
height: 100%;
padding: 20px;
}
.time-axis {
position: absolute;
bottom: 20px;
left: 60px;
right: 20px;
height: 2px;
background: #333;
}
.time-marker {
position: absolute;
bottom: 10px;
transform: translateX(-50%);
font-size: 11px;
color: #666;
white-space: nowrap;
}
.cert-track {
position: absolute;
left: 60px;
right: 20px;
height: 40px;
margin: 10px 0;
}
.cert-bar {
position: absolute;
height: 100%;
border-radius: 4px;
overflow: hidden;
cursor: pointer;
transition: opacity 0.2s;
}
.cert-bar:hover {
opacity: 0.8;
}
.ca-cert {
background: linear-gradient(135deg, #2196F3 0%, #1976D2 100%);
border: 2px solid #1565C0;
}
.leaf-cert {
background: linear-gradient(135deg, #4CAF50 0%, #388E3C 100%);
border: 2px solid #2E7D32;
}
.invalid-cert {
background: repeating-linear-gradient(
45deg,
#ff5252,
#ff5252 10px,
#ff8a80 10px,
#ff8a80 20px
);
border: 2px solid #d32f2f;
}
.renewal-window {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: rgba(255, 193, 7, 0.3);
border-left: 2px dashed #FFC107;
}
.cert-label {
position: absolute;
left: 10px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-weight: 600;
color: white;
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
pointer-events: none;
}
.legend {
display: flex;
gap: 30px;
margin: 20px 0;
padding: 15px;
background: #f9f9f9;
border-radius: 8px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
font-size: 14px;
}
.legend-box {
width: 30px;
height: 20px;
border-radius: 3px;
}
.stats {
margin-top: 30px;
padding: 20px;
background: #f0f0f0;
border-radius: 8px;
}
.stat-item {
margin: 10px 0;
font-size: 14px;
}
.warning {
background: #fff3cd;
border: 1px solid #ffeaa7;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
color: #856404;
}
.error {
background: #f8d7da;
border: 1px solid #f5c6cb;
padding: 15px;
border-radius: 8px;
margin: 20px 0;
color: #721c24;
}
.tooltip {
position: fixed;
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
pointer-events: none;
z-index: 1000;
white-space: nowrap;
display: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 15px;
}
.controls {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="container">
<h1>Certificate Renewal Timeline Visualizer</h1>
<div class="description">
This tool visualizes certificate lifecycles and renewal behavior in cert-manager.
It helps identify potential issues when CA and leaf certificates have similar durations,
where a leaf might be renewed just before the CA expires, resulting in an invalid certificate.
</div>
<div class="controls">
<div class="control-group">
<label for="caDuration">CA Duration (days)</label>
<input type="number" id="caDuration" value="1095" min="1" max="3650">
</div>
<div class="control-group">
<label for="leafDuration">Leaf Duration (days)</label>
<input type="number" id="leafDuration" value="1095" min="1" max="3650">
</div>
<div class="control-group">
<label for="renewBefore">Renew Before (% of duration)</label>
<input type="number" id="renewBefore" value="33" min="1" max="50" step="1">
</div>
<div class="control-group">
<label for="leafCount">Number of Leaf Certs</label>
<input type="number" id="leafCount" value="3" min="1" max="10">
</div>
<div class="control-group">
<label for="randomness">Renewal Randomness (hours)</label>
<input type="number" id="randomness" value="24" min="0" max="168">
</div>
<div class="control-group">
<label for="simulationDays">Simulation Duration (days)</label>
<input type="number" id="simulationDays" value="1200" min="365" max="3650">
</div>
<div class="control-group" style="align-self: flex-end;">
<button onclick="runSimulation()">Run Simulation</button>
</div>
</div>
<div class="legend">
<div class="legend-item">
<div class="legend-box ca-cert"></div>
<span>CA Certificate</span>
</div>
<div class="legend-item">
<div class="legend-box leaf-cert"></div>
<span>Valid Leaf Certificate</span>
</div>
<div class="legend-item">
<div class="legend-box invalid-cert"></div>
<span>Invalid Leaf Certificate</span>
</div>
<div class="legend-item">
<div class="legend-box" style="background: rgba(255, 193, 7, 0.3); border-left: 2px dashed #FFC107;"></div>
<span>Renewal Window</span>
</div>
</div>
<div id="alerts"></div>
<div class="timeline-container">
<div class="timeline" id="timeline">
<div class="time-axis"></div>
</div>
</div>
<div class="stats" id="stats"></div>
</div>
<div class="tooltip" id="tooltip"></div>
<script>
let certData = {
ca: [],
leaves: []
};
function addRandomHours(date, baseHours, randomHours) {
const randomOffset = (Math.random() - 0.5) * 2 * randomHours;
return new Date(date.getTime() + (baseHours + randomOffset) * 60 * 60 * 1000);
}
function runSimulation() {
const caDurationDays = parseInt(document.getElementById('caDuration').value);
const leafDurationDays = parseInt(document.getElementById('leafDuration').value);
const renewBeforePercent = parseInt(document.getElementById('renewBefore').value);
const leafCount = parseInt(document.getElementById('leafCount').value);
const randomnessHours = parseInt(document.getElementById('randomness').value);
const simulationDays = parseInt(document.getElementById('simulationDays').value);
// Clear previous data
certData = { ca: [], leaves: [] };
// Calculate renewal windows
const caRenewalDays = Math.floor(caDurationDays * renewBeforePercent / 100);
const leafRenewalDays = Math.floor(leafDurationDays * renewBeforePercent / 100);
// Initialize CA certificate
let currentDate = new Date();
let caStart = new Date(currentDate);
let caEnd = new Date(caStart.getTime() + caDurationDays * 24 * 60 * 60 * 1000);
certData.ca.push({ start: caStart, end: caEnd, renewalWindow: caRenewalDays });
// Initialize leaf certificates with staggered start times
for (let i = 0; i < leafCount; i++) {
const staggerDays = Math.random() * 30; // Stagger initial certs by up to 30 days
const leafStart = new Date(currentDate.getTime() + staggerDays * 24 * 60 * 60 * 1000);
const leafEnd = new Date(leafStart.getTime() + leafDurationDays * 24 * 60 * 60 * 1000);
certData.leaves.push([{
start: leafStart,
end: leafEnd,
renewalWindow: leafRenewalDays,
isValid: true,
signingCA: certData.ca[0],
signingCAIndex: 1
}]);
}
// Simulate renewals
const endDate = new Date(currentDate.getTime() + simulationDays * 24 * 60 * 60 * 1000);
// Simulate CA renewals
let lastCA = certData.ca[0];
while (lastCA.end < endDate) {
const renewalWindowStart = new Date(lastCA.end.getTime() - caRenewalDays * 24 * 60 * 60 * 1000);
const renewalTime = addRandomHours(renewalWindowStart, 0, randomnessHours);
const newCAStart = renewalTime;
const newCAEnd = new Date(newCAStart.getTime() + caDurationDays * 24 * 60 * 60 * 1000);
certData.ca.push({
start: newCAStart,
end: newCAEnd,
renewalWindow: caRenewalDays
});
lastCA = certData.ca[certData.ca.length - 1];
}
// Simulate leaf renewals
for (let i = 0; i < leafCount; i++) {
let lastLeaf = certData.leaves[i][0];
while (lastLeaf.end < endDate) {
const renewalWindowStart = new Date(lastLeaf.end.getTime() - leafRenewalDays * 24 * 60 * 60 * 1000);
const renewalTime = addRandomHours(renewalWindowStart, 0, randomnessHours);
// Find the active CA at renewal time
// In cert-manager, once a new CA is created, it becomes the active CA
// The active CA is the one with the highest index whose start time <= renewal time
let signingCA = null;
let signingCAIndex = -1;
for (let j = certData.ca.length - 1; j >= 0; j--) {
const ca = certData.ca[j];
// If this CA has started by renewal time, it's the active one
if (ca.start <= renewalTime) {
signingCA = ca;
signingCAIndex = j;
break;
}
}
// Verify the CA is still valid at renewal time
if (signingCA && renewalTime > signingCA.end) {
console.error('Error: No valid CA at renewal time', renewalTime);
signingCA = null;
}
const newLeafStart = renewalTime;
const newLeafEnd = new Date(newLeafStart.getTime() + leafDurationDays * 24 * 60 * 60 * 1000);
// Check if this leaf will outlive its signing CA
const isValid = signingCA && newLeafEnd <= signingCA.end;
certData.leaves[i].push({
start: newLeafStart,
end: newLeafEnd,
renewalWindow: leafRenewalDays,
isValid: isValid,
signingCA: signingCA,
signingCAIndex: signingCAIndex + 1
});
lastLeaf = certData.leaves[i][certData.leaves[i].length - 1];
}
}
renderTimeline();
calculateStats();
}
function renderTimeline() {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '<div class="time-axis"></div>';
if (!certData.ca.length) return;
const startTime = certData.ca[0].start.getTime();
const endTime = Math.max(
...certData.ca.map(c => c.end.getTime()),
...certData.leaves.flat().map(c => c.end.getTime())
);
const totalDuration = endTime - startTime;
// Calculate responsive timeline width - much more compact
const containerWidth = document.querySelector('.timeline-container').clientWidth;
const availableWidth = Math.max(containerWidth - 80, 800); // Minimum 800px, but try to fit container
const daysInSimulation = totalDuration / (24 * 60 * 60 * 1000);
// Use a scaling factor that makes the timeline fit reasonably
// Start with trying to fit in available width, but ensure minimum readability
let pixelsPerDay = Math.max(0.3, Math.min(1.0, availableWidth / daysInSimulation));
const timelineWidth = Math.max(availableWidth, daysInSimulation * pixelsPerDay);
timeline.style.width = timelineWidth + 'px';
// Add time markers - adjust frequency based on timeline width
const totalDays = daysInSimulation;
let markerInterval;
if (totalDays <= 730) { // 2 years or less
markerInterval = 90 * 24 * 60 * 60 * 1000; // 3 months
} else if (totalDays <= 1460) { // 4 years or less
markerInterval = 180 * 24 * 60 * 60 * 1000; // 6 months
} else {
markerInterval = 365 * 24 * 60 * 60 * 1000; // 1 year
}
const trackWidth = timelineWidth - 80; // Account for left and right padding
for (let time = startTime; time <= endTime; time += markerInterval) {
const marker = document.createElement('div');
marker.className = 'time-marker';
const leftOffset = 60 + ((time - startTime) / totalDuration) * trackWidth;
marker.style.left = leftOffset + 'px';
marker.textContent = new Date(time).toLocaleDateString();
timeline.appendChild(marker);
}
// Render CA certificates
let caTrackTop = 60;
// Add CA section label
const caLabel = document.createElement('div');
caLabel.style.position = 'absolute';
caLabel.style.left = '5px';
caLabel.style.top = (caTrackTop - 20) + 'px';
caLabel.style.fontSize = '12px';
caLabel.style.fontWeight = '600';
caLabel.style.color = '#666';
caLabel.textContent = 'CA:';
timeline.appendChild(caLabel);
certData.ca.forEach((cert, index) => {
const track = document.createElement('div');
track.className = 'cert-track';
track.style.top = caTrackTop + 'px';
const bar = createCertBar(cert, startTime, totalDuration, 'ca-cert', `CA #${index + 1}`);
track.appendChild(bar);
timeline.appendChild(track);
caTrackTop += 50;
});
// Render leaf certificates - each renewal on its own line
let leafTrackTop = caTrackTop + 30;
certData.leaves.forEach((leafChain, chainIndex) => {
// Add a label for this leaf chain
const chainLabel = document.createElement('div');
chainLabel.style.position = 'absolute';
chainLabel.style.left = '5px';
chainLabel.style.top = leafTrackTop + 'px';
chainLabel.style.fontSize = '12px';
chainLabel.style.fontWeight = '600';
chainLabel.style.color = '#666';
chainLabel.textContent = `Leaf ${chainIndex + 1}:`;
timeline.appendChild(chainLabel);
leafChain.forEach((cert, index) => {
const track = document.createElement('div');
track.className = 'cert-track';
track.style.top = leafTrackTop + 'px';
const className = cert.isValid ? 'leaf-cert' : 'invalid-cert';
const bar = createCertBar(cert, startTime, totalDuration, className, `Leaf ${chainIndex + 1}.${index + 1}`);
track.appendChild(bar);
timeline.appendChild(track);
leafTrackTop += 50;
});
// Add extra spacing between different leaf chains
leafTrackTop += 20;
});
timeline.style.height = (leafTrackTop + 80) + 'px';
}
function createCertBar(cert, startTime, totalDuration, className, label) {
const bar = document.createElement('div');
bar.className = 'cert-bar ' + className;
const startPercent = (cert.start.getTime() - startTime) / totalDuration * 100;
const widthPercent = (cert.end.getTime() - cert.start.getTime()) / totalDuration * 100;
bar.style.left = startPercent + '%';
bar.style.width = widthPercent + '%';
// Add renewal window
const renewalWindow = document.createElement('div');
renewalWindow.className = 'renewal-window';
const renewalPercent = cert.renewalWindow * 24 * 60 * 60 * 1000 / (cert.end.getTime() - cert.start.getTime()) * 100;
renewalWindow.style.width = renewalPercent + '%';
bar.appendChild(renewalWindow);
// Add label
const labelEl = document.createElement('div');
labelEl.className = 'cert-label';
labelEl.textContent = label;
bar.appendChild(labelEl);
// Add tooltip
bar.addEventListener('mouseenter', (e) => showTooltip(e, cert, label));
bar.addEventListener('mousemove', (e) => moveTooltip(e));
bar.addEventListener('mouseleave', hideTooltip);
return bar;
}
function showTooltip(event, cert, label) {
const tooltip = document.getElementById('tooltip');
const duration = Math.floor((cert.end - cert.start) / (24 * 60 * 60 * 1000));
let content = `${label}<br>`;
content += `Start: ${cert.start.toLocaleDateString()}<br>`;
content += `End: ${cert.end.toLocaleDateString()}<br>`;
content += `Duration: ${duration} days<br>`;
content += `Renewal Window: ${cert.renewalWindow} days`;
if (cert.signingCA) {
const caEnd = cert.signingCA.end.toLocaleDateString();
const caIndex = cert.signingCAIndex || 'Unknown';
content += `<br>Signed by: CA #${caIndex}`;
content += `<br>CA expires: ${caEnd}`;
if (!cert.isValid) {
content += `<br><strong style="color: #ff5252;">⚠️ Outlives CA!</strong>`;
}
}
tooltip.innerHTML = content;
tooltip.style.display = 'block';
tooltip.style.left = event.clientX + 10 + 'px';
tooltip.style.top = event.clientY - 30 + 'px';
}
function moveTooltip(event) {
const tooltip = document.getElementById('tooltip');
if (tooltip.style.display === 'block') {
tooltip.style.left = event.clientX + 10 + 'px';
tooltip.style.top = event.clientY - 30 + 'px';
}
}
function hideTooltip() {
document.getElementById('tooltip').style.display = 'none';
}
function calculateStats() {
const stats = document.getElementById('stats');
const alerts = document.getElementById('alerts');
let invalidCount = 0;
let totalLeafCerts = 0;
let issues = [];
certData.leaves.forEach((chain, chainIndex) => {
chain.forEach((cert, certIndex) => {
totalLeafCerts++;
if (!cert.isValid) {
invalidCount++;
issues.push({
leaf: `Leaf ${chainIndex + 1}.${certIndex + 1}`,
renewedAt: cert.start.toLocaleDateString(),
leafExpires: cert.end.toLocaleDateString(),
caExpires: cert.signingCA ? cert.signingCA.end.toLocaleDateString() : 'Unknown',
signingCA: cert.signingCAIndex || 'Unknown'
});
}
});
});
// Generate stats HTML
let statsHTML = '<h3>Simulation Statistics</h3>';
statsHTML += `<div class="stat-item"><strong>Total CA Certificates:</strong> ${certData.ca.length}</div>`;
statsHTML += `<div class="stat-item"><strong>Total Leaf Certificates:</strong> ${totalLeafCerts}</div>`;
statsHTML += `<div class="stat-item"><strong>Invalid Certificates:</strong> ${invalidCount} (${(invalidCount/totalLeafCerts*100).toFixed(1)}%)</div>`;
if (invalidCount > 0) {
statsHTML += '<div class="stat-item"><strong>Issues Found:</strong></div>';
statsHTML += '<ul style="margin: 10px 0; font-size: 13px;">';
issues.forEach(issue => {
statsHTML += `<li>${issue.leaf} renewed on ${issue.renewedAt}, signed by CA #${issue.signingCA}, expires ${issue.leafExpires} but CA expires ${issue.caExpires}</li>`;
});
statsHTML += '</ul>';
}
stats.innerHTML = statsHTML;
// Generate alerts
let alertsHTML = '';
if (invalidCount > 0) {
alertsHTML += `<div class="error">
<strong>⚠️ Critical Issue Detected:</strong> ${invalidCount} leaf certificate(s) will outlive their signing CA.
This will cause validation failures in your Cilium clustermesh setup.
</div>`;
}
const caDuration = parseInt(document.getElementById('caDuration').value);
const leafDuration = parseInt(document.getElementById('leafDuration').value);
if (leafDuration >= caDuration * 0.8) {
alertsHTML += `<div class="warning">
<strong>⚠️ Warning:</strong> Leaf certificate duration (${leafDuration} days) is very close to CA duration (${caDuration} days).
This increases the risk of renewal timing issues. Consider using a much shorter leaf duration (e.g., 180-365 days).
</div>`;
}
alerts.innerHTML = alertsHTML;
}
// Run initial simulation
runSimulation();
// Handle window resize
window.addEventListener('resize', () => {
if (certData.ca.length > 0) {
renderTimeline();
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment