DEV Community

Alessio
Alessio

Posted on Originally published at py-forge.com

Auditing Expiring and Unattended Credentials with Python

Local development servers, build runners, and staging machines frequently accumulate unattended secrets. Files containing TLS certificates, JSON Web Tokens (JWT), service account configurations, and SSH private keys are written during maintenance or testing and then forgotten. Over time, these files create two distinct operational risks:

  1. Silent Expiration: Certificates and signed access tokens expire unexpectedly, breaking scripts, local proxies, and automated tasks.
  2. Hygiene Failures: Key pairs and API credentials sit unrotated for months, often stored with permissive file permissions that allow any user or local process on the host to read them.

Many teams rely on complex scanning platforms or external SaaS tools to monitor certificate lifetimes, but checking static credential files on a filesystem does not require heavyweight infrastructure.

This tutorial walks through CredentialWatch, an alert and expiration inspection tool written strictly with the Python standard library. It inspects files for upcoming or past expiration dates, stale modification times, and dangerous permission bits.


Install & Usage

The tool uses only Python standard library modules (argparse, base64, json, os, re, stat, sys, tempfile, datetime, pathlib). There are no third-party packages to install via pip.

To get started, save the script as alert_expiry_check_for_unattended_credentials.py on your machine.

You can verify the script runs using Python 3:

python alert_expiry_check_for_unattended_credentials.py --help
Enter fullscreen mode Exit fullscreen mode

To run a scan against specific configuration directories or key stores, invoke the script and provide the directories or files you want to inspect:

python alert_expiry_check_for_unattended_credentials.py /etc/ssl/certs /var/run/secrets ./config
Enter fullscreen mode Exit fullscreen mode

When scanning, you can configure thresholds to define how early warnings should appear and when unrotated files are classified as stale:

python alert_expiry_check_for_unattended_credentials.py ./certs --warn-days 45 --critical-days 10 --stale-days 60
Enter fullscreen mode Exit fullscreen mode

The tool scans target directories recursively, evaluates discovered credentials, and outputs an ASCII table summarizing the severity, credential type, days remaining until expiry, days since the file was last modified, and file path.


How It Works

CredentialWatch processes targets without external cryptographic libraries like OpenSSL or cryptography. It parses structured data formats natively to pull validity timestamps.

1. Extracting Validity Dates from X.509 Certificates

To read expiration dates from PEM-formatted certificates, the tool decodes base64 certificate content into raw DER bytes and iterates through the ASN.1 sequence to find the validity window.

The extract_x509_dates function scans for ASN.1 UTCTime (tag 0x17) and GeneralizedTime (tag 0x18) bytes:

def extract_x509_dates(der_bytes: bytes) -> Tuple[Optional[datetime], Optional[datetime]]:
    """
    Extract notBefore and notAfter from DER-encoded X.509 certificate bytes
    by locating ASN.1 time tags within the validity sequence.
    """
    not_before = None
    not_after = None
    idx = 0
    total = len(der_bytes)

    while idx < total - 13:
        tag = der_bytes[idx]
        if tag in (0x17, 0x18):
            length = der_bytes[idx + 1]
            content_start = idx + 2
            if length & 0x80:
                len_bytes = length & 0x7F
                if content_start + len_bytes <= total:
                    length = int.from_bytes(der_bytes[content_start : content_start + len_bytes], "big")
                    content_start += len_bytes
                else:
                    idx += 1
                    continue

            content_end = content_start + length
            if content_end <= total:
                val = _parse_asn1_time(der_bytes[content_start:content_end], tag)
                if val:
                    if not_before is None:
                        not_before = val
                    elif not_after is None:
                        not_after = val
                        break
                    idx = content_end
                    continue
        idx += 1

    return not_before, not_after
Enter fullscreen mode Exit fullscreen mode

Once the tags are identified, the byte values are decoded into UTC timestamps representing the certificate's notBefore and notAfter dates.

2. File Inspection and Permission Auditing

For every regular file encountered, inspect_credential_file analyzes three distinct layers: permissions, file modification age (staleness), and content-based credential metadata.

    # Check permission hygiene (flag world-readable or group-writable credential storage)
    mode = st.st_mode
    perm_flags = []
    if mode & stat.S_IROTH:
        perm_flags.append("WORLD_READABLE")
    if mode & stat.S_IWOTH:
        perm_flags.append("WORLD_WRITABLE")
    if mode & stat.S_IWGRP:
        perm_flags.append("GROUP_WRITABLE")

    now = datetime.now(timezone.utc)
    mtime = _timestamp_to_utc_datetime(st.st_mtime) or now
    days_since_modified = (now - mtime).total_seconds() / 86400.0

    # Read content sample up to 1 MB
    try:
        if st.st_size > 1_048_576:
            with file_path.open("rb") as f:
                raw_head = f.read(1_048_576)
            text = raw_head.decode("utf-8", errors="ignore")
        else:
            text = file_path.read_text(encoding="utf-8", errors="ignore")
    except Exception:
        return None
Enter fullscreen mode Exit fullscreen mode

The inspection logic applies the following checks:

  • File Permissions: Tests whether the file mode contains S_IROTH (world-readable), S_IWOTH (world-writable), or S_IWGRP (group-writable).
  • JWT Parsing: Scans the text for patterns matching three base64-encoded segments (eyJ...). If found, it decodes the payload to extract the exp (expiration) and iat (issued at) claims.
  • SSH Keys: Looks for standard PEM headers such as BEGIN RSA PRIVATE KEY or BEGIN OPENSSH PRIVATE KEY. Because SSH private keys do not embed an expiration timestamp, the scanner evaluates them based on modification age and permission hygiene.
  • Service Account JSON: Inspects JSON structures for known credential markers (like private_key or client_secret) and parses fields such as expires_at, expiry, expiration_timestamp, or valid_until.

Based on these checks, the tool assigns an overall severity level: EXPIRED, CRITICAL, WARNING, STALE, INSECURE_PERMS, or OK.


Limitations

While standard library parsing avoids dependency overhead, this implementation has specific technical constraints:

  • No Cryptographic Signature Verification: The script only extracts and decodes claims. It does not verify whether a JWT signature is valid or whether a certificate is trusted by a local certificate authority.
  • Sample Size Limit: The file reader caps its initial read buffer at 1 MB (1_048_576 bytes). Credentials placed beyond this initial offset in large bundle files will not be detected.
  • Heuristic ASN.1 Parsing: The X.509 date extractor searches for raw time tags (0x17 and 0x18) sequentially. While functional for standard X.509 structures, it is not a complete ASN.1 schema validator and will skip non-standard encodings.
  • File Modification as Rotation Proxy: Staleness calculations rely on file system modification times (mtime). If a deployment pipeline recreates identical files on disk during every deployment without rotating the underlying credential, mtime will reset, masking the true age of the credential.

Frequently Asked Questions

Does this tool require OpenSSL or Python cryptography libraries installed?

No. All certificate parsing is performed by decoding the base64 PEM block into DER format and scanning raw ASN.1 tag bytes directly. JWT extraction uses built-in base64 decoding and the standard json module.

How does the tool determine if an SSH private key is compromised or stale?

SSH private keys do not contain embedded expiration timestamps. The scanner flags an SSH private key if its file permissions are too open (such as world-readable permissions) or if its filesystem modification date exceeds the configured staleness threshold (by default, 90 days without rotation).

Can it parse custom expiration fields in proprietary JSON configurations?

The script checks for common standard fields: expires_at, expiry, expiration_timestamp, and valid_until formatted in ISO 8601 timestamps (such as %Y-%m-%dT%H:%M:%SZ). Custom date formats or alternative property names require adjusting the candidate field list inside inspect_credential_file.


Full version and the rest of the toolkit: Alert/expiry check for unattended credentials on PyForge. Product page: https://py-forge.com/products/alert-expiry-check-for-unattended-credentials

Top comments (0)