{"id":48766425,"url":"https://github.com/cplieger/cert-converter","last_synced_at":"2026-05-30T01:05:03.381Z","repository":{"id":342034177,"uuid":"1172565447","full_name":"cplieger/cert-converter","owner":"cplieger","description":"Automated PEM-to-PFX certificate converter with file watching","archived":false,"fork":false,"pushed_at":"2026-05-22T13:28:36.000Z","size":90,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-22T14:43:53.135Z","etag":null,"topics":["certificate","distroless","docker","fsnotify","golang","homelab","pem","pfx","pkcs12","ssl","tls"],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cplieger.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-04T13:04:20.000Z","updated_at":"2026-05-22T12:48:12.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/cplieger/cert-converter","commit_stats":null,"previous_names":["cplieger/docker-cert-converter","cplieger/cert-converter"],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/cplieger/cert-converter","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fcert-converter","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fcert-converter/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fcert-converter/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fcert-converter/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cplieger","download_url":"https://codeload.github.com/cplieger/cert-converter/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cplieger%2Fcert-converter/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33676192,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["certificate","distroless","docker","fsnotify","golang","homelab","pem","pfx","pkcs12","ssl","tls"],"created_at":"2026-04-13T08:01:42.618Z","updated_at":"2026-05-30T01:05:03.369Z","avatar_url":"https://github.com/cplieger.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# cert-converter\n\n![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)\n[![GitHub release](https://img.shields.io/github/v/release/cplieger/cert-converterer)](https://github.com/cplieger/cert-converterer/releases)\n[![Image Size](https://ghcr-badge.egpl.dev/cplieger/cert-converter/size)](https://github.com/cplieger/cert-converterer/pkgs/container/cert-convert)\n![Platforms](https://img.shields.io/badge/platforms-amd64%20%7C%20arm64-blue)\n![base: Distroless](https://img.shields.io/badge/base-Distroless_nonroot-4285F4?logo=google)\n\nAutomatically converts PEM certificates to PFX format whenever they renew — set it and forget it.\n\n## What it does\n\nWatches a certificate directory using fsnotify (with polling fallback) for\nnew or changed PEM certificate files. When a change is detected, it reads\nthe certificate chain and private key, then produces a PKCS#12 (.pfx) file —\nfor example, if Caddy generates PEM certificates and you have apps that only\naccept PFX/PKCS#12 files (e.g. some Synology services, .NET apps, or\nWindows-based tools), point the input directory to Caddy's certificate folder\nand this container will automatically produce PFX files whenever certificates\nare renewed. SHA-256 change detection skips unchanged certificates. Supports\nmodern2023, modern2026, and legacy PFX encoding profiles. Includes a CLI\nhealth probe for distroless Docker healthchecks (file-based, no HTTP server\nor open port).\n\nThis is a distroless, rootless container — it runs as `nonroot` on\n`gcr.io/distroless/static` with no shell or package manager.\n\n### Why this design\n\n- **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.\n- **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.\n- **SHA-256 skip-unchanged** — avoids unnecessary PFX regeneration by fingerprinting input files, reducing disk writes and keeping output timestamps meaningful.\n- **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.\n\n## Quick start\n\nThe image is published to both GHCR (`ghcr.io/cplieger/cert-converter`) and Docker Hub (`cplieger/cert-converter`) — identical contents, use whichever you prefer.\n\n```yaml\nservices:\n  cert-converter:\n    image: ghcr.io/cplieger/cert-converter:latest\n    container_name: cert-converter\n    restart: unless-stopped\n    user: \"1000:1000\"  # match your host user\n\n    environment:\n      TZ: \"Europe/Paris\"\n      PFX_PASSWORD: \"your-pfx-password\"\n      FALLBACK_SCAN_HOURS: \"6\"  # fsnotify fallback interval\n      PFX_ENCODER: \"modern2023\"  # modern2023, modern2026, or legacy\n\n    volumes:\n      - \"/path/to/pem/certificates:/input:ro\"\n      - \"/path/to/pfx/output:/output:rw\"\n```\n\n## Configuration reference\n\n### Environment variables\n\n| Variable | Description | Default | Required |\n|----------|-------------|---------|----------|\n| `TZ` | Container timezone | `Europe/Paris` | No |\n| `PFX_PASSWORD` | Password embedded in generated PFX files | - | Yes |\n| `FALLBACK_SCAN_HOURS` | Hours between full directory re-scans (fallback when fsnotify misses events) | `6` | No |\n| `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 |\n\n### Volumes\n\n| Mount | Description |\n|-------|-------------|\n| `/input` | PEM certificate directory (read-only) |\n| `/output` | PFX output directory |\n\n## Healthcheck\n\nThe 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.\n\n## Code quality\n\n| Metric | Value |\n|--------|-------|\n| [Test Coverage](https://go.dev/blog/cover) | 69.5% |\n| Tests | 123 |\n| [Cyclomatic Complexity](https://en.wikipedia.org/wiki/Cyclomatic_complexity) (avg) | 5.3 |\n| [Cognitive Complexity](https://www.sonarsource.com/docs/CognitiveComplexity.pdf) (avg) | 6.1 |\n| [Mutation Efficacy](https://en.wikipedia.org/wiki/Mutation_testing) | 90.5% (59 runs) |\n| Test Framework | Property-based ([rapid](https://github.com/flyingmutant/rapid)) + table-driven |\n\nThe test suite validates all user-facing functionality: PEM certificate\nparsing (RSA, ECDSA, Ed25519, chain handling, corrupt input), PFX\nencoding round-trips across all encoder profiles, SHA-256 change\ndetection with file size guards, fsnotify event handling with debounce\nlogic, and the full processing pipeline (skip unchanged, reconvert on\nchange, nested directories, error recovery). Property-based tests\nverify that parsing functions never panic on arbitrary input and that\nround-trips preserve certificate data.\n\nNot tested: the filesystem watcher loop and polling fallback — these\nare event-driven I/O paths that can't be unit tested meaningfully.\nValidated by Docker healthchecks in production (the health probe\nconfirms the last processing cycle succeeded).\n\n## Security\n\n**No vulnerabilities found.** All scans clean across 10 tools.\n\n| Tool | Result |\n|------|--------|\n| [govulncheck](https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck) | No vulnerabilities in call graph |\n| [golangci-lint](https://golangci-lint.run/) (gosec, gocritic) | 0 issues |\n| [trivy](https://trivy.dev/) | 0 vulnerabilities |\n| [grype](https://github.com/anchore/grype) | 0 vulnerabilities |\n| [gitleaks](https://github.com/gitleaks/gitleaks) | No secrets detected |\n| [semgrep](https://semgrep.dev/) | 1 info (false positive) |\n| [hadolint](https://github.com/hadolint/hadolint) | Clean |\n\nThis app has a minimal attack surface: no network listener, no\nHTTP server, no exposed ports. It reads PEM files from a mounted\ndirectory and writes PFX files to another. Runs as `nonroot` on\na distroless base image with no shell or package manager.\n\n**Details for advanced users:** File paths are hardcoded\n(`/input`, `/output`), not configurable via env vars. File reads\nare TOCTOU-safe (stat + read from same handle) with a 10 MB cap.\nPFX writes use atomic temp-file + rename. The semgrep finding is\nthe `/tmp/.healthy` health marker, a fixed-path zero-byte file\nin a single-process container.\n\n## Dependencies\n\nUpdated automatically via [Renovate](https://github.com/renovatebot/renovate) and pinned by digest. Builds carry signed SBOMs and provenance attestations verifiable with `gh attestation verify`.\n\n| Dependency | Version | Source |\n|------------|---------|--------|\n| golang | `1.26-alpine` | [Go](https://hub.docker.com/_/golang) |\n| gcr.io/distroless/static-debian13 | `nonroot` | [Distroless](https://github.com/GoogleContainerTools/distroless) |\n| github.com/fsnotify/fsnotify | `v1.10.1` | [GitHub](https://github.com/fsnotify/fsnotify) |\n| pgregory.net/rapid | `v1.3.0` | [pkg.go.dev](https://pkg.go.dev/pgregory.net/rapid) |\n| software.sslmate.com/src/go-pkcs12 | `v0.7.1` | [SSLMate](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) |\n\n## Credits\n\nThis is an original tool that builds upon [Go crypto/x509 + go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12).\n\n## Contributing\n\nIssues and pull requests are welcome. Please open an issue first for\nlarger changes so the approach can be discussed before implementation.\n\n## Disclaimer\n\nThese 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.\n\nThis 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.\n\n## License\n\nThis project is licensed under the [GNU General Public License v3.0](LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fcert-converter","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcplieger%2Fcert-converter","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcplieger%2Fcert-converter/lists"}