https://github.com/cplieger/cert-converter
Automated PEM-to-PFX certificate converter with file watching
https://github.com/cplieger/cert-converter
certificate distroless docker fsnotify golang homelab pem pfx pkcs12 ssl tls
Last synced: 23 days ago
JSON representation
Automated PEM-to-PFX certificate converter with file watching
- Host: GitHub
- URL: https://github.com/cplieger/cert-converter
- Owner: cplieger
- License: gpl-3.0
- Created: 2026-03-04T13:04:20.000Z (4 months ago)
- Default Branch: main
- Last Pushed: 2026-05-22T13:28:36.000Z (about 1 month ago)
- Last Synced: 2026-05-22T14:43:53.135Z (about 1 month ago)
- Topics: certificate, distroless, docker, fsnotify, golang, homelab, pem, pfx, pkcs12, ssl, tls
- Language: Go
- Size: 87.9 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- License: LICENSE
Awesome Lists containing this project
README
# cert-converter

[](https://github.com/cplieger/cert-converterer/releases)
[](https://github.com/cplieger/cert-converterer/pkgs/container/cert-convert)


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).
This is a distroless, rootless container — it runs as `nonroot` on
`gcr.io/distroless/static` with no shell or package manager.
### 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.
```yaml
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:
TZ: "Europe/Paris"
PFX_PASSWORD: "your-pfx-password"
FALLBACK_SCAN_HOURS: "6" # fsnotify fallback interval
PFX_ENCODER: "modern2023" # modern2023, modern2026, or legacy
volumes:
- "/path/to/pem/certificates:/input:ro"
- "/path/to/pfx/output:/output:rw"
```
## Configuration reference
### Environment variables
| Variable | Description | Default | Required |
|----------|-------------|---------|----------|
| `TZ` | Container timezone | `Europe/Paris` | No |
| `PFX_PASSWORD` | Password embedded in generated PFX files | - | Yes |
| `FALLBACK_SCAN_HOURS` | Hours between full directory re-scans (fallback when fsnotify misses events) | `6` | No |
| `PFX_ENCODER` | PFX encoding profile — modern2023 (AES-256-CBC + SHA-256, default), modern2026 (AES-256-CBC + PBMAC1, requires OpenSSL 3.4.0+), or legacy (3DES + SHA-1 for older devices). See [go-pkcs12 documentation](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12#pkg-variables). | `modern2023` | No |
### Volumes
| Mount | Description |
|-------|-------------|
| `/input` | PEM certificate directory (read-only) |
| `/output` | PFX output directory |
## Healthcheck
The container includes a built-in health probe: after each successful certificate processing cycle, 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. The container becomes unhealthy when the input directory is unreadable, PEM parsing fails, or PFX writes fail — any error during a processing cycle removes the marker. It auto-recovers on the next successful cycle (triggered by an fsnotify event or the fallback timer) without requiring a restart.
## Code quality
| Metric | Value |
|--------|-------|
| [Test Coverage](https://go.dev/blog/cover) | 69.5% |
| Tests | 123 |
| [Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) (avg) | 5.3 |
| [Cognitive Complexity](https://www.sonarsource.com/docs/CognitiveComplexity.pdf) (avg) | 6.1 |
| [Mutation Efficacy](https://en.wikipedia.org/wiki/Mutation_testing) | 90.5% (59 runs) |
| Test Framework | Property-based ([rapid](https://github.com/flyingmutant/rapid)) + table-driven |
The test suite validates all user-facing functionality: PEM certificate
parsing (RSA, ECDSA, Ed25519, chain handling, corrupt input), PFX
encoding round-trips across all encoder profiles, SHA-256 change
detection with file size guards, fsnotify event handling with debounce
logic, and the full processing pipeline (skip unchanged, reconvert on
change, nested directories, error recovery). Property-based tests
verify that parsing functions never panic on arbitrary input and that
round-trips preserve certificate data.
Not tested: the filesystem watcher loop and polling fallback — these
are event-driven I/O paths that can't be unit tested meaningfully.
Validated by Docker healthchecks in production (the health probe
confirms the last processing cycle succeeded).
## Security
**No vulnerabilities found.** All scans clean across 10 tools.
| Tool | Result |
|------|--------|
| [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) | No vulnerabilities in call graph |
| [golangci-lint](https://golangci-lint.run/) (gosec, gocritic) | 0 issues |
| [trivy](https://trivy.dev/) | 0 vulnerabilities |
| [grype](https://github.com/anchore/grype) | 0 vulnerabilities |
| [gitleaks](https://github.com/gitleaks/gitleaks) | No secrets detected |
| [semgrep](https://semgrep.dev/) | 1 info (false positive) |
| [hadolint](https://github.com/hadolint/hadolint) | Clean |
This app has a minimal attack surface: no network listener, no
HTTP server, no exposed ports. It reads PEM files from a mounted
directory and writes PFX files to another. Runs as `nonroot` on
a distroless base image with no shell or package manager.
**Details for advanced users:** File paths are hardcoded
(`/input`, `/output`), not configurable via env vars. File reads
are TOCTOU-safe (stat + read from same handle) with a 10 MB cap.
PFX writes use atomic temp-file + rename. The semgrep finding is
the `/tmp/.healthy` health marker, a fixed-path zero-byte file
in a single-process container.
## Dependencies
Updated automatically via [Renovate](https://github.com/renovatebot/renovate) and pinned by digest. Builds carry signed SBOMs and provenance attestations verifiable with `gh attestation verify`.
| Dependency | Version | Source |
|------------|---------|--------|
| golang | `1.26-alpine` | [Go](https://hub.docker.com/_/golang) |
| gcr.io/distroless/static-debian13 | `nonroot` | [Distroless](https://github.com/GoogleContainerTools/distroless) |
| github.com/fsnotify/fsnotify | `v1.10.1` | [GitHub](https://github.com/fsnotify/fsnotify) |
| pgregory.net/rapid | `v1.3.0` | [pkg.go.dev](https://pkg.go.dev/pgregory.net/rapid) |
| software.sslmate.com/src/go-pkcs12 | `v0.7.1` | [SSLMate](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) |
## Credits
This is an original tool that builds upon [Go crypto/x509 + go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/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
These images are built with care and follow security best practices, but they are intended for **homelab use**. No guarantees of fitness for production environments. Use at your own risk.
This project was built with AI-assisted tooling using [Claude Opus](https://www.anthropic.com/claude) and [Kiro](https://kiro.dev). 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](LICENSE).