cert-converter

command module
v1.3.1 Latest Latest
Warning

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

Go to latest
Published: Jul 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 using fsnotify (with polling fallback) for new or changed PEM certificate files. When a change is detected, it reads the certificate chain and private key, then produces a PKCS#12 (.pfx) file — for example, if Caddy generates PEM certificates and you have apps that only accept PFX/PKCS#12 files (e.g. some Synology services, .NET apps, or Windows-based tools), point the input directory to Caddy's certificate folder and this container will automatically produce PFX files whenever certificates are renewed. SHA-256 change detection skips unchanged certificates. Supports modern2023, modern2026, and legacy PFX encoding profiles. Includes a CLI health probe for distroless Docker healthchecks (file-based, no HTTP server or open port).

Why this design
  • Distroless and rootless — runs on gcr.io/distroless/static:nonroot with no shell or package manager, minimizing attack surface and eliminating entire classes of container escapes.
  • fsnotify with polling fallback — reacts to certificate changes in real time, but falls back to periodic full scans so network mounts and edge cases never cause missed renewals.
  • SHA-256 skip-unchanged — avoids unnecessary PFX regeneration by fingerprinting input files, reducing disk writes and keeping output timestamps meaningful.
  • No HTTP server, no open ports — the container has zero network listeners; health is reported via a file-based probe, leaving nothing exposed to the network.

Quick start

The image is published to both GHCR (ghcr.io/cplieger/cert-converter) and Docker Hub (cplieger/cert-converter) — identical contents, 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"
      FALLBACK_SCAN_HOURS: "6"  # fsnotify fallback interval
      PFX_ENCODER: "modern2023"  # modern2023, modern2026, legacy, or legacyrc2

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

Configuration reference

Environment variables
Variable Description Default Required
PFX_PASSWORD Password embedded in generated PFX files. Required: the container refuses to start when this is empty unless PFX_ALLOW_EMPTY_PASSWORD=true is set. - Yes
PFX_ALLOW_EMPTY_PASSWORD Opt out of the empty-password guard. When PFX_PASSWORD is empty the container refuses to start; set this to true to allow startup anyway. 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 (fallback when fsnotify misses events). Only an explicit 0 or false disables the periodic fallback; with it off, a missed fsnotify event (common on network mounts) is not recovered until the next change, so a renewal can be skipped. A set-but-empty (FALLBACK_SCAN_HOURS=""), whitespace, or invalid value uses the 6h default (like omitting the key), so a blank never silently disables it. 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 accepted as an alias for modern2023, and legacy is recorded as legacydes in startup logs. See go-pkcs12 documentation. modern2023 No
LOG_LEVEL Minimum log level — debug, info (default), warn, or error (case-insensitive; accepts slog offsets such as info+2). Set to debug to surface per-certificate skip reasons (orphan, unchanged, unreadable subdir) and filesystem-event detail that are otherwise suppressed. An unrecognized value falls back to info. info No

FALLBACK_SCAN_HOURS ceiling: a value above 87600 (10 years) is clamped to that ceiling and logs a WARN.

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 container includes a built-in health probe: after each processing cycle with no conversion failures, the main process creates a marker file at /tmp/.healthy; the health subcommand (/cert-watcher health) checks for this file and exits 0 if it exists.

Health answers a single operational question — should an orchestrator restart this container? — so it tracks only failures a restart could plausibly clear. The container becomes unhealthy when the /input root itself cannot be read, or when 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 (triggered by an fsnotify event or the fallback timer) without requiring 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, so 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

No vulnerabilities found. All scans clean across the full scanner suite.

Tool Result
govulncheck No vulnerabilities in call graph
golangci-lint (gosec, gocritic) 0 issues
trivy 0 vulnerabilities
grype 0 vulnerabilities
gitleaks No secrets detected
semgrep 1 info (false positive)
hadolint Clean

This app has a minimal attack surface: it reads PEM files from a mounted directory and writes PFX files to another, with no network listener or open port (see Why this design).

Details for advanced users: 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 + read from the same handle) with a 10 MB cap. PFX writes use atomic temp-file + rename.

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 Distroless
github.com/fsnotify/fsnotify GitHub
pgregory.net/rapid pkg.go.dev
software.sslmate.com/src/go-pkcs12 SSLMate

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 Opus and Kiro. The human maintainer defines architecture, supervises implementation, and makes all final decisions.

License

This project is licensed under the GNU General Public License v3.0.

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