# nebula-cert-manager

CLI tool for Nebula overlay network certificate lifecycle management.

Manages the full certificate lifecycle: CA creation, certificate issuance, revocation, sync/re-issue, and config generation. PKI data (CA, certs, keys) is stored in a SOPS-encrypted registry file; all network configuration (IPs, lighthouses, firewall, relays) lives in `nebula.config.yml`.

## Requirements

- Python 3.12+
- [nebula-cert](https://github.com/slackhq/nebula) in PATH (or via `--nebula-cert`)
- [sops](https://github.com/getsops/sops) in PATH (or via `--sops`)
- [age](https://github.com/FiloSottile/age) for encryption key management

## SOPS / age Setup

The registry is encrypted with [SOPS](https://github.com/getsops/sops) using [age](https://github.com/FiloSottile/age) keys. You need to configure this before running `init` or `import-ca`.

### 1. Generate an age key (one-time)

```sh
age-keygen -o ~/.config/sops/age/keys.txt
```

Note the public key from the output (starts with `age1...`).

### 2. Configure SOPS

Create a `.sops.yaml` in the directory where you'll run `nebula-cert-manager` (or any parent directory):

```yaml
creation_rules:
  - path_regex: .*\.sops\.yaml$
    age: age1your-public-key-here
```

Alternatively, set the `SOPS_AGE_RECIPIENTS` environment variable:

```sh
export SOPS_AGE_RECIPIENTS=age1your-public-key-here
```

SOPS will automatically use `~/.config/sops/age/keys.txt` for decryption.

## Installation

```sh
uv tool install .
```

Or run directly from source:

```sh
uv run nebula-cert-manager --help
```

## Quick Start

1. Initialize a new registry with a CA:

```sh
nebula-cert-manager init --age age1your-public-key-here
```

2. Create `nebula.config.yml` in the registry directory with your network layout:

```yaml
network:
  cidr: 10.43.0.0/16

hosts:
  defaults:
    listen_port: 4242
  hosts:
    my-lighthouse:
      nebula_ip: 10.43.0.1
      public_endpoints:
        - "203.0.113.1:4242"
    dtatarkin-laptop:
      nebula_ip: 10.43.1.1
```

3. Issue a client certificate:

```sh
nebula-cert-manager issue --name dtatarkin-laptop
```

4. Generate config and cert files for the client:

```sh
nebula-cert-manager config --name dtatarkin-laptop --output-dir ./out
```

This creates `./out/dtatarkin-laptop/` with `client.crt`, `client.key`, and `config.yml`.

## Commands

| Command      | Description                                                  |
|--------------|--------------------------------------------------------------|
| `init`       | Initialize registry directory and create a new CA            |
| `import-ca`  | Import existing CA into a new registry                       |
| `issue`      | Issue a new client certificate                               |
| `revoke`     | Revoke a client certificate                                  |
| `delete`     | Delete a certificate record from the registry                |
| `list`       | List certificates (table or JSON)                            |
| `config`     | Generate client config and cert/key files                    |
| `info`       | Show registry and CA information                             |
| `sync`       | Reconcile registry with nebula.config.yml                    |

## Global Options

```
--registry-dir PATH    Registry directory (default: $NEBULA_CERT_MANAGER_DIR or ./nebula-registry)
--nebula-cert PATH     Path to nebula-cert binary
--sops PATH            Path to sops binary
--age KEY              AGE public key for SOPS encryption (default: $NEBULA_CERT_MANAGER_AGE)
```

## Registry

The registry directory contains two key files:

- `registry.sops.yaml` — SOPS-encrypted file holding **only PKI data**: CA cert/key, client certs/keys, fingerprints, and revocation status.
- `nebula.config.yml` — Plain YAML file holding **all network configuration**: CIDR, host IPs, lighthouses, firewall rules, and host overrides.

```
nebula-registry/
├── registry.sops.yaml   # SOPS-encrypted PKI data
├── ca.crt               # CA certificate (plaintext, public)
└── nebula.config.yml    # Network configuration (plaintext)
```

Commit both files to git. The registry holds secrets (encrypted); the config is readable by anyone with repo access.

## Network Configuration

`nebula.config.yml` is the single source of truth for all network configuration. The `issue` and `sync` commands require it to be present with `network` and `hosts` sections.

### Example `nebula.config.yml`

```yaml
network:
  cidr: 10.43.0.0/16

hosts:
  defaults:
    tun_dev: nebula1          # global default TUN device name
    listen_port: 33333        # global default listen port
  hosts:
    lighthouse-1:
      nebula_ip: 10.43.0.1
      am_relay: true          # this host acts as a relay
      public_endpoints:       # presence of this field marks host as a lighthouse
        - "203.0.113.1:4242"
    lighthouse-2:
      nebula_ip: 10.43.0.2
      public_endpoints:
        - "203.0.113.2:4242"
        - "198.51.100.2:4242"
    web-server:
      nebula_ip: 10.43.1.1
    dtatarkin-laptop:
      nebula_ip: 10.43.1.2
      relays:                 # use these relays for NAT traversal
        - 10.43.0.1
    dtatarkin-mobile:
      nebula_ip: 10.43.1.3
      tun_dev: nebula-mobile   # override for a specific host

firewall:
  security_groups:
    icmp:
      port: any
      proto: icmp
      direction: any        # "in", "out", or "any"
    out-any:
      port: any
      proto: any
      direction: out
    in-http:
      port: 80
      proto: tcp
      direction: in
  host_groups:              # cert group membership per host
    dtatarkin-laptop:
      - admin
  firewall_rules:           # per-host firewall assignments
    dtatarkin-laptop:
      all:                  # peer group: "all" → host: any
        - icmp
        - out-any
      admin:                # peer group: "admin" → group: admin
        - in-http
  firewall_default:         # fallback for hosts not in firewall_rules
    all:
      - icmp
      - out-any
```

### Network

| Field  | Description                          |
|--------|--------------------------------------|
| `cidr` | Nebula network CIDR (e.g., `10.43.0.0/16`) |

### Hosts

Every host that needs a certificate must be listed under `hosts.hosts` with a unique `nebula_ip`. IPs are validated for uniqueness at config parse time.

A host is automatically treated as a **lighthouse** if it has `public_endpoints` set. There is no separate lighthouses section.

- **defaults** — Global default values applied to all hosts (`tun_dev`, `listen_port`, `relays`).
- **hosts** — Per-host configuration. Each key is a host name matching a certificate name.

| Field              | Required | Description                                                       |
|--------------------|----------|-------------------------------------------------------------------|
| `nebula_ip`        | Yes      | Nebula VPN IP address for this host                               |
| `public_endpoints` | No       | List of public `ip:port` endpoints (makes host a lighthouse)      |
| `tun_dev`          | No       | TUN device name override (`tun.dev` in Nebula config)             |
| `listen_port`      | No       | Listen port override (required for lighthouses, via defaults or per-host)   |
| `am_relay`         | No       | Whether this host acts as a Nebula relay (default: `false`)       |
| `relays`           | No       | List of relay nebula IPs to use for NAT traversal                 |

When generating config for a lighthouse host, `am_lighthouse` is set to `true` and that host is excluded from its own lighthouse list. Regular clients get all lighthouses in `static_host_map` and `lighthouse.hosts`. Lighthouses require `listen_port` to be set (via `defaults` or per-host); it is not auto-derived from `public_endpoints`.

### Firewall

- **security_groups** — Named rules with `port`, `proto` (any/tcp/udp/icmp), and `direction` (in/out/any). Rules with `direction: any` are added to both inbound and outbound.
- **host_groups** — Nebula certificate groups assigned per host. These are merged into the cert at `issue` and `sync` time.
- **firewall_rules** — Per-host firewall assignments. Each key is a host name, mapping peer groups to lists of security group names. The peer group `all` maps to `host: any`; named peer groups map to `group: <name>`.
- **firewall_default** — Fallback rules for hosts not listed in `firewall_rules`.

When the `firewall` section is absent, the generated config uses defaults: allow all outbound, ICMP-only inbound.

## Development

Requires [uv](https://docs.astral.sh/uv/) and [just](https://github.com/casey/just).

```sh
uv sync                  # install dependencies
just check               # format + lint + test
just fmt                 # format sources
just lint                # run linter
just test                # run tests
just test -v             # run tests with verbose output
```
