cert-converter

command module
v1.3.17 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 19, 2026 License: GPL-2.0, GPL-3.0 Imports: 14 Imported by: 0

README

cert-converter

Image Size Platforms Test coverage Mutation OpenSSF Best Practices OpenSSF Scorecard SBOM

Automatically converts PEM certificates to PFX format whenever they renew. Set it and forget it.

What it does

Watches a certificate directory for new or changed PEM certificate files and converts each one, chain plus private key, into a PKCS#12 (.pfx) file. The typical use: Caddy renews PEM certificates, but some of your apps only accept PFX (some Synology services, .NET apps, Windows-based tools). Point /input at Caddy's certificate folder and fresh PFX files appear on every renewal. SHA-256 change detection skips unchanged certificates; modern2023, modern2026, and legacy encoding profiles cover both current and older consumers.

Why this design
  • Distroless and rootless: runs on gcr.io/distroless/static-debian13:nonroot with no shell or package manager, minimizing the attack surface.
  • fsnotify with polling fallback: reacts to certificate changes in real time, and periodic full scans catch anything fsnotify misses (network mounts, edge cases), so renewals are never skipped.
  • SHA-256 skip-unchanged: fingerprints input files to skip pointless PFX regeneration, reducing disk writes and keeping output timestamps meaningful.
  • No HTTP server, no open ports: the container has zero network listeners; health is a file-based probe, so nothing is exposed to the network.

Quick start

The image is published to both GHCR (ghcr.io/cplieger/cert-converter) and Docker Hub (cplieger/cert-converter); the contents are identical, use whichever you prefer.

services:
  cert-converter:
    image: ghcr.io/cplieger/cert-converter:latest
    container_name: cert-converter
    restart: unless-stopped
    user: "1000:1000"  # match your host user

    environment:
      PFX_PASSWORD: "your-pfx-password"  # required
      PFX_ENCODER: "modern2023"  # modern2023, modern2026, legacy, or legacyrc2

    volumes:
      - "/path/to/pem/certificates:/input:ro"
      - "/path/to/pfx/output:/output"

Configuration reference

Environment variables
Variable Description Default Required
PFX_PASSWORD Password embedded in generated PFX files. The container refuses to start when this is empty unless PFX_ALLOW_EMPTY_PASSWORD=true is set. - Yes
PFX_ALLOW_EMPTY_PASSWORD Set to true to let the container start with an empty PFX_PASSWORD. Generated PFX files then protect the embedded private key with an empty password (effectively no protection); not recommended. false No
FALLBACK_SCAN_HOURS Hours between full directory re-scans, the fallback for fsnotify events missed on network mounts and similar edge cases. Only an explicit 0 or false disables it, leaving a missed event unrecovered until the next change. An empty, whitespace, or invalid value uses the 6h default, so a blank never silently disables the safety net; a value above 87600 (10 years) is clamped to that ceiling and logs a WARN. 6 No
PFX_ENCODER PFX encoding profile: modern2023 (AES-256-CBC + SHA-256, default), modern2026 (AES-256-CBC + PBMAC1, requires OpenSSL 3.4.0+), legacy (3DES + SHA-1 for older devices), or legacyrc2 (RC2-40 + SHA-1, only for very old devices). modern is an alias for modern2023, and legacy is recorded as legacydes in startup logs. See the go-pkcs12 documentation. modern2023 No
LOG_LEVEL Minimum log level: debug, info, warn, or error (case-insensitive; slog offsets such as info+2 work). debug surfaces per-certificate skip reasons (orphan, unchanged, unreadable subdir) and filesystem-event detail. An unrecognized value falls back to info. info No
Volumes
Mount Description
/input PEM certificate directory (read-only)
/output PFX output directory

Alerting

cert-converter has no metrics endpoint; its operational state is in its logs. Ship the container's logs to Loki (Grafana Alloy's Docker log discovery does this with no configuration) and evaluate these with Loki's ruler; firing alerts deliver through your Alertmanager exactly like Prometheus metric alerts.

groups:
  - name: cert-converter
    rules:
      - alert: CertConverterConversionFailed
        expr: |
          sum by (container) (count_over_time(
            {container="cert-converter"} |= `scan complete`
            |~ ` (failed|unreadable)=[1-9]` [15m]
          )) > 0
        for: 0m
        labels:
          severity: warning
        annotations:
          summary: "cert-converter failed to convert a certificate"
          description: >
            A scan logged failed>0 or unreadable>0 (PEM parse, PFX write, or
            input read failure); the affected .pfx is stale or missing. Check
            /input permissions and the certificate chain.
      - alert: CertConverterScanStalled
        expr: |
          absent_over_time({container="cert-converter"} |= `scan complete` [8h])
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "cert-converter has not completed a scan in 8h"
          description: >
            cert-converter emits a `scan complete` line at least every
            FALLBACK_SCAN_HOURS (default 6h). None in 8h while the container is
            up means the fsnotify watch and the fallback timer are both wedged;
            certificates silently stop converting. Restart the container.

Thresholds and the severity label are starting points; adjust the stall window to your FALLBACK_SCAN_HOURS and the container selector to your deployment, and route by whatever labels your Alertmanager uses.

Healthcheck

The image bakes in a health probe. After each processing cycle with no conversion failures, the main process writes a marker file at /tmp/.healthy. The health subcommand (/cert-watcher health) exits 0 when the marker exists and, while the fallback rescan is enabled, is fresher than three FALLBACK_SCAN_HOURS intervals. A staler marker means the watch loop is wedged, so the probe fails and the orchestrator restarts the container. Setting FALLBACK_SCAN_HOURS to 0/false disables both the fallback and this staleness deadline.

Health answers one question: should an orchestrator restart this container? It therefore tracks only failures a restart could plausibly clear. The container becomes unhealthy when the /input root itself cannot be read or a certificate fails to convert (PEM or key parse error, cert/key mismatch, or PFX write failure). It auto-recovers on the next clean cycle (fsnotify event or fallback timer) without a restart.

An unreadable sub-path under /input (e.g. one certificate directory with the wrong permissions or owner) is a steady-state misconfiguration a restart would not fix. It is logged as a warning and its certificates are skipped; it does not flip the container unhealthy. Fix the directory permissions or run the container as a UID that can read it.

Security

The attack surface is small: the container reads PEM files from one mounted directory and writes PFX files to another, with no network listener or open port. It runs as a non-root user on a distroless base with no shell or package manager (see Why this design), so there is nothing to expose or firewall; keep the /input mount read-only as in the quick start.

File paths are hardcoded (/input, /output), not configurable via env vars. Input reads are confined to /input through an os.Root, so a symlink planted in the input tree cannot redirect a read outside it. Reads are TOCTOU-safe (stat and read from the same handle) with a 10 MB cap, and malformed PEM or key input is rejected and logged rather than converted. PFX writes use an atomic temp-file + rename.

One accepted scanner finding: semgrep flags the fixed /tmp/.healthy health-marker path as a predictable temp file. The path is a deliberate contract between the main process and the health probe inside the container's own filesystem, not shared state an attacker can pre-create. Live scan results are on the repository's Security tab.

Dependencies

Updated automatically via Renovate and pinned by digest. Builds carry signed SBOMs and provenance attestations verifiable with gh attestation verify.

Dependency Source
golang Go
gcr.io/distroless/static-debian13 Distroless
github.com/fsnotify/fsnotify GitHub
pgregory.net/rapid pkg.go.dev
software.sslmate.com/src/go-pkcs12 SSLMate
github.com/cplieger/health GitHub
github.com/cplieger/slogx GitHub
github.com/cplieger/atomicfile/v2 GitHub

Credits

This is an original tool that builds upon Go crypto/x509 + go-pkcs12.

Contributing

Issues and pull requests are welcome. Please open an issue first for larger changes so the approach can be discussed before implementation.

Disclaimer

This project is built with care and follows security best practices, but it is intended for personal / self-hosted use. No guarantees of fitness for production environments. Use at your own risk.

This project was built with AI-assisted tooling using Claude, GPT, and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

GPL-3.0. See LICENSE.

Documentation

Overview

Package main watches a PEM certificate directory and converts changed certificates to PFX/PKCS#12 on every renewal.

Directories

Path Synopsis
internal
config
Package config provides environment-based configuration for cert-converter.
Package config provides environment-based configuration for cert-converter.
convert
Package convert provides PEM parsing and PFX encoding utilities.
Package convert provides PEM parsing and PFX encoding utilities.
process
Package process provides the certificate scanning and conversion orchestration.
Package process provides the certificate scanning and conversion orchestration.
testcerts
Package testcerts provides shared certificate generation helpers for tests.
Package testcerts provides shared certificate generation helpers for tests.
watch
Package watch provides filesystem watching with fsnotify and poll fallback.
Package watch provides filesystem watching with fsnotify and poll fallback.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL