Certificate Inventory

Complexity: Low
Duration: 1-4 hours (depending on size)
Prerequisite: Access to all systems

Complete inventory of all certificates as a basis for migration.


Why Inventory?

Reason Description
——–————-
Scope How many certificates need to be migrated?
Algorithms Which algorithms are in use?
Expiry dates When can certificates be migrated during renewal?
Dependencies Which systems are interconnected?
Risks Where are critical certificates?

Inventory Sources

flowchart TB subgraph LOCAL["LOCAL"] L1[File system] L2[Certificate stores] L3[Configuration files] end subgraph NETWORK["NETWORK"] N1[TLS endpoints] N2[LDAP/AD] N3[HSM] end subgraph MGMT["MANAGEMENT"] M1[CA database] M2[CMDB] M3[Monitoring] end subgraph OUTPUT["OUTPUT"] O[Inventory CSV] end L1 & L2 & L3 --> O N1 & N2 & N3 --> O M1 & M2 & M3 --> O


Linux: File System Scan

#!/bin/bash
# cert-inventory-linux.sh
 
OUTPUT="inventory-linux-$(hostname)-$(date +%Y%m%d).csv"
echo "Path,Subject,Issuer,Algorithm,KeyLength,ValidFrom,ValidTo,DaysRemaining,Serial,SANs" > "$OUTPUT"
 
# Standard directories
CERT_DIRS=(
    "/etc/ssl/certs"
    "/etc/pki/tls/certs"
    "/etc/nginx/ssl"
    "/etc/apache2/ssl"
    "/opt/*/ssl"
    "/var/lib/docker/volumes/*/ssl"
)
 
for dir in "${CERT_DIRS[@]}"; do
    for cert in $(find $dir -name "*.pem" -o -name "*.crt" -o -name "*.cer" 2>/dev/null); do
        # Only certificates (no keys/CSRs)
        openssl x509 -in "$cert" -noout 2>/dev/null || continue
 
        subject=$(openssl x509 -in "$cert" -subject -noout | sed 's/subject=//' | tr ',' ';')
        issuer=$(openssl x509 -in "$cert" -issuer -noout | sed 's/issuer=//' | tr ',' ';')
        algo=$(openssl x509 -in "$cert" -text -noout | grep "Signature Algorithm" | head -1 | awk '{print $3}')
        keysize=$(openssl x509 -in "$cert" -text -noout | grep "Public-Key:" | grep -oP '\d+')
        not_before=$(openssl x509 -in "$cert" -startdate -noout | cut -d= -f2)
        not_after=$(openssl x509 -in "$cert" -enddate -noout | cut -d= -f2)
        days_left=$(( ($(date -d "$not_after" +%s) - $(date +%s)) / 86400 ))
        serial=$(openssl x509 -in "$cert" -serial -noout | cut -d= -f2)
        sans=$(openssl x509 -in "$cert" -text -noout | grep -A1 "Subject Alternative Name" | tail -1 | tr ',' ';')
 
        echo "\"$cert\",\"$subject\",\"$issuer\",\"$algo\",\"$keysize\",\"$not_before\",\"$not_after\",\"$days_left\",\"$serial\",\"$sans\"" >> "$OUTPUT"
    done
done
 
echo "Inventory completed: $OUTPUT"
echo "Certificates found: $(tail -n +2 "$OUTPUT" | wc -l)"

Windows: Certificate Store Scan

# Cert-Inventory-Windows.ps1
 
param(
    [string]$OutputPath = "inventory-windows-$env:COMPUTERNAME-$(Get-Date -Format 'yyyyMMdd').csv"
)
 
$results = @()
 
# Search all certificate stores
$stores = @(
    @{ Location = "LocalMachine"; Name = "My" },
    @{ Location = "LocalMachine"; Name = "Root" },
    @{ Location = "LocalMachine"; Name = "CA" },
    @{ Location = "LocalMachine"; Name = "WebHosting" },
    @{ Location = "CurrentUser"; Name = "My" }
)
 
foreach ($store in $stores) {
    $storePath = "Cert:\$($store.Location)\$($store.Name)"
 
    Get-ChildItem $storePath -ErrorAction SilentlyContinue | ForEach-Object {
        $cert = $_
        $daysLeft = ($cert.NotAfter - (Get-Date)).Days
 
        # Extract SANs
        $sanExt = $cert.Extensions | Where-Object { $_.Oid.Value -eq "2.5.29.17" }
        $sans = if ($sanExt) { $sanExt.Format($false) } else { "" }
 
        $results += [PSCustomObject]@{
            Store = "$($store.Location)\$($store.Name)"
            Subject = $cert.Subject
            Issuer = $cert.Issuer
            Algorithm = $cert.SignatureAlgorithm.FriendlyName
            KeySize = $cert.PublicKey.Key.KeySize
            NotBefore = $cert.NotBefore
            NotAfter = $cert.NotAfter
            DaysLeft = $daysLeft
            Serial = $cert.SerialNumber
            Thumbprint = $cert.Thumbprint
            SANs = $sans
            HasPrivateKey = $cert.HasPrivateKey
        }
    }
}
 
$results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
Write-Host "Inventory completed: $OutputPath"
Write-Host "Certificates found: $($results.Count)"

Network: TLS Endpoint Scan

#!/bin/bash
# cert-inventory-network.sh
 
OUTPUT="inventory-network-$(date +%Y%m%d).csv"
ENDPOINTS_FILE="endpoints.txt"
 
echo "Endpoint,Subject,Issuer,Algorithm,KeyLength,ValidTo,Days,Serial" > "$OUTPUT"
 
# Endpoints from file or CMDB/DNS
# Format: hostname:port
cat "$ENDPOINTS_FILE" | while read endpoint; do
    host=${endpoint%:*}
    port=${endpoint#*:}
    [ -z "$port" ] && port=443
 
    echo "Scanning $host:$port..."
 
    cert_info=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | openssl x509 -text -noout 2>/dev/null)
 
    if [ -n "$cert_info" ]; then
        subject=$(echo "$cert_info" | grep "Subject:" | head -1 | sed 's/.*Subject: //' | tr ',' ';')
        issuer=$(echo "$cert_info" | grep "Issuer:" | head -1 | sed 's/.*Issuer: //' | tr ',' ';')
        algo=$(echo "$cert_info" | grep "Signature Algorithm" | head -1 | awk '{print $3}')
        keysize=$(echo "$cert_info" | grep "Public-Key:" | grep -oP '\d+')
        not_after=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | openssl x509 -enddate -noout | cut -d= -f2)
        days_left=$(( ($(date -d "$not_after" +%s) - $(date +%s)) / 86400 ))
        serial=$(echo | openssl s_client -connect "$host:$port" -servername "$host" 2>/dev/null | openssl x509 -serial -noout | cut -d= -f2)
 
        echo "\"$endpoint\",\"$subject\",\"$issuer\",\"$algo\",\"$keysize\",\"$not_after\",\"$days_left\",\"$serial\"" >> "$OUTPUT"
    else
        echo "\"$endpoint\",\"CONNECTION FAILED\",\"\",\"\",\"\",\"\",\"\",\"\"" >> "$OUTPUT"
    fi
done
 
echo "Network inventory completed: $OUTPUT"

CA Database Inventory

#!/bin/bash
# cert-inventory-ca.sh - From OpenSSL CA index
 
CA_INDEX="/etc/pki/CA/index.txt"
OUTPUT="inventory-ca-$(date +%Y%m%d).csv"
 
echo "Status,Serial,Expiry,Subject,File" > "$OUTPUT"
 
while IFS=$'\t' read -r status expiry revoke serial unknown subject; do
    # Status: V=Valid, R=Revoked, E=Expired
    status_text=""
    case "$status" in
        V) status_text="Valid" ;;
        R) status_text="Revoked" ;;
        E) status_text="Expired" ;;
    esac
 
    # Convert expiry date (YYMMDDHHMMSSZ format)
    expiry_formatted=$(date -d "20${expiry:0:2}-${expiry:2:2}-${expiry:4:2}" +%Y-%m-%d 2>/dev/null)
 
    # Certificate file
    cert_file="/etc/pki/CA/newcerts/${serial}.pem"
 
    echo "\"$status_text\",\"$serial\",\"$expiry_formatted\",\"$subject\",\"$cert_file\"" >> "$OUTPUT"
done < "$CA_INDEX"
 
echo "CA inventory completed: $OUTPUT"
echo "Certificates:"
echo "  Valid:   $(grep -c "^\"Valid\"" "$OUTPUT")"
echo "  Revoked: $(grep -c "^\"Revoked\"" "$OUTPUT")"
echo "  Expired: $(grep -c "^\"Expired\"" "$OUTPUT")"

Analysis & Reporting

#!/usr/bin/env python3
# analyze-inventory.py
 
import pandas as pd
from datetime import datetime
 
# Load all inventory files
df = pd.concat([
    pd.read_csv('inventory-linux*.csv'),
    pd.read_csv('inventory-windows*.csv'),
    pd.read_csv('inventory-network*.csv')
])
 
# Analysis
print("=== Inventory Summary ===\n")
 
print(f"Total certificates: {len(df)}")
 
print("\n--- By Algorithm ---")
print(df['Algorithm'].value_counts())
 
print("\n--- By Key Length ---")
print(df['KeyLength'].value_counts())
 
print("\n--- Expiry Analysis ---")
df['DaysRemaining'] = pd.to_numeric(df['DaysRemaining'], errors='coerce')
print(f"Expired:          {len(df[df['DaysRemaining'] < 0])}")
print(f"< 30 days:        {len(df[(df['DaysRemaining'] >= 0) & (df['DaysRemaining'] < 30)])}")
print(f"30-90 days:       {len(df[(df['DaysRemaining'] >= 30) & (df['DaysRemaining'] < 90)])}")
print(f"90-365 days:      {len(df[(df['DaysRemaining'] >= 90) & (df['DaysRemaining'] < 365)])}")
print(f"> 1 year:         {len(df[df['DaysRemaining'] >= 365])}")
 
print("\n--- Migration Potential ---")
rsa = len(df[df['Algorithm'].str.contains('rsa', case=False, na=False)])
ecdsa = len(df[df['Algorithm'].str.contains('ecdsa|ec', case=False, na=False)])
hybrid = len(df[df['Algorithm'].str.contains('ml-dsa|hybrid', case=False, na=False)])
print(f"RSA (to migrate):     {rsa}")
print(f"ECDSA (to migrate):   {ecdsa}")
print(f"Hybrid (already):     {hybrid}")
 
# Export for migration planning
df.to_excel('inventory-complete.xlsx', index=False)
print("\nComplete inventory exported: inventory-complete.xlsx")

Dashboard Template

Category Count Migration
———-——-———–
Algorithm
RSA-2048 150 → Hybrid
RSA-4096 30 → Hybrid
ECDSA P-256 80 → Hybrid
ECDSA P-384 20 → Hybrid
ML-DSA/Hybrid 5 Done
Expiry
< 30 days 12 Migrate immediately
30-90 days 25 Phase 1
90-365 days 100 Phase 2
> 1 year 148 Phase 3
Criticality
External-facing 40 Priority 1
Internal-critical 80 Priority 2
Development 165 Priority 3

Checklist

# Checkpoint Done
——————
1 All Linux servers scanned
2 All Windows servers scanned
3 All TLS endpoints scanned
4 CA database exported
5 Data consolidated
6 Analysis performed
7 Migration plan created


« <- Rollback Strategy | -> Operator Scenarios »


Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional

Zuletzt geändert: on 2026/01/30 at 01:33 AM