Metadata-Version: 2.4
Name: certbundle
Version: 1.0.0
Summary: Request grouped SAN certificates through Certbot.
Project-URL: Homepage, https://www.veen.world/
Project-URL: Repository, https://github.com/kevinveenbirkenbach/certbot-domain-bundler
Author-email: Kevin Veen-Birkenbach <kevin@veen.world>
License: MIT
License-File: LICENSE
Keywords: certbot,certificates,cli,letsencrypt,san
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# 📦 Certbot Domain Bundler

[![GitHub Sponsors](https://img.shields.io/badge/Sponsor-GitHub%20Sponsors-blue?logo=github)](https://github.com/sponsors/kevinveenbirkenbach) [![Patreon](https://img.shields.io/badge/Support-Patreon-orange?logo=patreon)](https://www.patreon.com/c/kevinveenbirkenbach) [![Buy Me a Coffee](https://img.shields.io/badge/Buy%20me%20a%20Coffee-Funding-yellow?logo=buymeacoffee)](https://buymeacoffee.com/kevinveenbirkenbach) [![PayPal](https://img.shields.io/badge/Donate-PayPal-blue?logo=paypal)](https://s.veen.world/paypaldonate)

**Certbot Domain Bundler** automates the creation of Let's Encrypt certificates for multiple domains using SANs (Subject Alternative Names).  
It intelligently groups domains and supports both DNS and webroot challenges.

---

## 🧭 How it works

```mermaid
flowchart TD
    A["certbundle --domains a.example.com,b.example.com,..."] --> B["hash = first 8 hex of sha256 over the sorted domain list"]
    A --> C{"--domain-categories given?"}
    C -- yes --> D["group by the longest matching category"]
    C -- no --> E["group by base domain SLD.TLD"]
    D --> F{"group larger than --chunk-size?"}
    E --> F
    F -- yes --> G["split the group into batches"]
    F -- no --> H["one batch per group"]
    G --> I
    H --> I["cert name = certbundle-HASH-NNNNN"]
    B --> I
    I --> J["certbot certonly, webroot or dns-PLUGIN"]
    J --> K["classify: updated / skipped / failed"]
    K --> L{"more batches?"}
    L -- yes --> I
    L -- no --> M["print the summary"]
    M --> N{"any batch failed?"}
    N -- yes --> O["exit 1"]
    N -- no --> P["exit 0"]
```

The batch counter runs across all groups, so the numbering is consecutive over the whole run. A failing batch never aborts the remaining ones.

---

## 🚀 Installation

```bash
pip install certbundle
```

pip is the single supported installation path.

After installation, usage instructions are available via:

```bash
certbundle --help
```

---

## 🔧 Requirements

- **Python 3.10+** 🐍
- **`certbot`** on `PATH`, plus the DNS plugin matching `--certbot-acme-challenge-method` when not using `webroot`

If `certbot` is missing, the command exits with code `127` and a one‑line error instead of a traceback.

---

## 🛠 Features

- Group multiple domains into a single SAN certificate
- Supports DNS-01 and HTTP-01 (webroot) ACME challenges
- Control DNS propagation wait time
- Wildcard domain support
- Test mode for Let's Encrypt staging environment
- Fully customizable via CLI parameters

---

## ⚙️ Certificate naming

Every certificate is named `certbundle-<hash>-<counter>`, where `<hash>` is the first 8 characters of the SHA‑256 over the **sorted** domain list and `<counter>` is a five‑digit batch number starting at `00001`.

Because the hash covers the whole domain set, changing the set changes every certificate name and causes certbot to issue new certificates rather than renew the existing ones.

---

## 📊 Result classification

Each batch is classified from certbot's exit code and output:

| Status | Trigger |
| --- | --- |
| `updated` | certbot exited `0` |
| `skipped:no-change` | `certificate not yet due for renewal` |
| `skipped:already-issued` | `already issued for this exact set of identifiers` |
| `skipped:rate-limit` | `too many certificates` |
| `skipped:service-down` | `the service is down for maintenance or had an internal error` |
| `failed:other-error` | any other non‑zero exit |

Every batch runs to completion — a failing batch never aborts the remaining ones.

### Exit codes

| Code | Meaning |
| --- | --- |
| `0` | No batch failed. Skipped batches do not fail the run. |
| `1` | At least one batch was classified `failed`. |
| `2` | Invalid command line arguments. |
| `127` | A required command is not installed. |

---

## 🧪 Development

```bash
make lint              # ruff check + ruff format --check
make format            # apply ruff format
make test              # unit + integration tests
make test-unit
make test-integration
make test-e2e          # install the package in a container and exercise the CLI
```

Tests run against the working tree — the `Makefile` puts `src/` on `PYTHONPATH`, so no install is needed. The integration tests stub `certbot` on `PATH`, so no certificate is ever requested.

---

## 📜 License

This project is licensed under the **MIT License**.

---

## 👤 Author

Developed by **Kevin Veen-Birkenbach**  
🌐 [https://www.veen.world](https://www.veen.world)
