> ## Documentation Index
> Fetch the complete documentation index at: https://docs.certgov.app/llms.txt
> Use this file to discover all available pages before exploring further.

# App Connectors

> Automate TLS certificate delivery for local applications — nginx, Apache, HAProxy, and any process that reads cert and key files from disk.

App Connectors automate certificate issuance and renewal for applications running on the **same host** as the `certforge-connector` agent. The connector generates the private key and CSR locally, submits the CSR to CertForge for issuance through your configured CA and Domain Trust Policy, writes the signed cert and key to the paths you specify, then runs a reload command so the application picks up the new certificate — all without manual steps or cron jobs.

```
[CertForge cloud] ←── poll every 30s ──── [certforge-connector]
                                                    │
                                         writes cert + key files
                                                    │
                                     [nginx / apache / app on this host]
```

## Prerequisites

* `certforge-connector` v0.3.3 or later installed and enrolled — see [certforge-connector](/guides/device-connector)
* A Domain Trust Policy covering the domain(s) you want to manage — see [Domain Trust Profiles](/concepts/domain-trust-profiles)
* The application process running as, or readable by, a user the connector can write files as (usually `root` when the connector runs as root)

## Step 1 — Register the application in CertForge

1. Go to **Integrations → App Connectors** in CertForge.
2. Click **+ Add Application**.
3. Fill in the fields:

| Field               | Example                    | Description                                                                                                   |
| ------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Name**            | `nginx-prod`               | Friendly label shown in the UI                                                                                |
| **Connector Agent** | `hq-connector`             | The enrolled agent that will manage this app                                                                  |
| **Domain (CN)**     | `api.example.com`          | Primary domain for the certificate                                                                            |
| **SANs**            | `www.api.example.com`      | Additional Subject Alternative Names (comma-separated; optional)                                              |
| **Trust Profile**   | `LetsEncrypt-prod`         | Domain Trust Policy governing issuance                                                                        |
| **Cert Path**       | `/etc/nginx/ssl/cert.pem`  | Where to write the full cert bundle (leaf + chain)                                                            |
| **Key Path**        | `/etc/nginx/ssl/key.pem`   | Where to write the private key                                                                                |
| **Chain Path**      | `/etc/nginx/ssl/chain.pem` | Intermediates only, without the leaf cert (optional — useful for apps that configure the CA chain separately) |
| **Reload Command**  | `nginx -s reload`          | Shell command to run after cert delivery                                                                      |

4. Click **Save + Request Cert** to save the app and immediately queue an issuance job. Use **Save Only** if you want to configure the app without triggering issuance yet (for example, to set up the file paths before the cert is needed).

No changes to `certforge-connector.yaml` are required — all app configuration is stored in CertForge and delivered to the connector with each job.

## Step 2 — Approve the request (if required)

If the Domain Trust Policy requires human approval, the job enters **Pending Approval** and appears in the **Approvals** queue. An authorized approver must approve it before issuance proceeds. If the DTP is set to auto-approve, issuance starts immediately.

## Step 3 — Verify delivery

On the next connector poll (within `poll_interval`, default 30 s), the connector:

1. Picks up the `cert_ready` job
2. Writes the private key to `key_path` (mode `0600`)
3. Writes the full cert bundle (leaf + chain) to `cert_path` (mode `0644`)
4. Writes the chain only to `chain_path` if configured
5. Runs the reload command
6. Marks the job done — the app's status updates to **Active** with the cert expiry date

Verify the cert is live:

```sh theme={null}
openssl s_client -connect localhost:443 -servername api.example.com \
  </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -dates
```

The connector log will show each step:

```
[app-connector] job abc123 (nginx-prod): generating RSA 2048 key + CSR for api.example.com
[app-connector] job abc123: private key written to /etc/nginx/ssl/key.pem
[app-connector] job abc123 (nginx-prod): CSR submitted — status: pending_approval
[app-connector] job abc123 (nginx-prod): cert_ready — delivering to /etc/nginx/ssl/cert.pem
[app-connector] job abc123: cert bundle written to /etc/nginx/ssl/cert.pem
[app-connector] job abc123: running reload: nginx -s reload
[app-connector] job abc123: reload complete
[app-connector] job abc123 (nginx-prod): complete — cert delivered and service reloaded
```

## Application recipes

The **+ Add App** form includes a **Use a template** dropdown that pre-fills the paths and reload command for common applications. Templates are a starting point — adjust paths to match your actual configuration before saving.

The connector runs reload commands via `sh -c`, so pipes, `&&`, and environment variable substitution all work. If the command exits non-zero, the job is marked **Failed** in CertForge and an alert fires if configured.

***

### nginx

| Field          | Value                                   |
| -------------- | --------------------------------------- |
| Cert Path      | `/etc/nginx/ssl/cert.pem`               |
| Key Path       | `/etc/nginx/ssl/key.pem`                |
| Chain Path     | `/etc/nginx/ssl/chain.pem` *(optional)* |
| Reload Command | `nginx -s reload`                       |

`cert.pem` receives the full bundle (leaf + intermediates + root), which is what `ssl_certificate` expects. `chain.pem` receives the intermediates-only chain for apps that configure `ssl_trusted_certificate` separately.

Adjust the directory to match your site's `ssl_certificate` directive — a common alternative is `/etc/nginx/certs/` or `/etc/ssl/nginx/`.

***

### Apache (Debian / Ubuntu)

| Field          | Value                                     |
| -------------- | ----------------------------------------- |
| Cert Path      | `/etc/apache2/ssl/cert.pem`               |
| Key Path       | `/etc/apache2/ssl/key.pem`                |
| Chain Path     | `/etc/apache2/ssl/chain.pem` *(optional)* |
| Reload Command | `systemctl reload apache2`                |

Point `SSLCertificateFile` at `cert.pem` and `SSLCertificateKeyFile` at `key.pem`. If using a separate chain file, set `SSLCertificateChainFile` to `chain.pem` (Apache 2.4.8+ supports the full chain in `SSLCertificateFile` so a separate chain file is often unnecessary).

***

### Apache (RHEL / CentOS / AlmaLinux)

| Field          | Value                                   |
| -------------- | --------------------------------------- |
| Cert Path      | `/etc/httpd/ssl/cert.pem`               |
| Key Path       | `/etc/httpd/ssl/key.pem`                |
| Chain Path     | `/etc/httpd/ssl/chain.pem` *(optional)* |
| Reload Command | `systemctl reload httpd`                |

Same configuration directives as the Debian variant; only the service name (`httpd` vs `apache2`) and default config paths differ.

***

### HAProxy

HAProxy reads the certificate and private key from a **single combined file** — it does not accept separate cert and key files. Use the reload command to concatenate them immediately after delivery:

| Field          | Value                                                                                                                    |
| -------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Cert Path      | `/etc/haproxy/certs/cert.pem`                                                                                            |
| Key Path       | `/etc/haproxy/certs/key.pem`                                                                                             |
| Chain Path     | *(leave blank)*                                                                                                          |
| Reload Command | `cat /etc/haproxy/certs/cert.pem /etc/haproxy/certs/key.pem > /etc/haproxy/certs/bundle.pem && systemctl reload haproxy` |

Point the `bind` line at `bundle.pem`:

```
bind *:443 ssl crt /etc/haproxy/certs/bundle.pem
```

The connector writes `cert.pem` and `key.pem` separately; the reload command concatenates them into `bundle.pem` before signalling HAProxy.

***

### Postfix + Dovecot (mail server)

Both Postfix and Dovecot read the same cert and key files, so a single App Connector entry covers both. The reload command signals both services:

| Field          | Value                                                        |
| -------------- | ------------------------------------------------------------ |
| Cert Path      | `/etc/ssl/mail/cert.pem`                                     |
| Key Path       | `/etc/ssl/mail/key.pem`                                      |
| Chain Path     | *(leave blank — Postfix reads the full chain from cert.pem)* |
| Reload Command | `systemctl reload postfix && systemctl reload dovecot`       |

In `main.cf`:

```
smtpd_tls_cert_file = /etc/ssl/mail/cert.pem
smtpd_tls_key_file  = /etc/ssl/mail/key.pem
```

In `dovecot.conf` (or `conf.d/10-ssl.conf`):

```
ssl_cert = </etc/ssl/mail/cert.pem
ssl_key  = </etc/ssl/mail/key.pem
```

***

### PostgreSQL

| Field          | Value                            |
| -------------- | -------------------------------- |
| Cert Path      | `/etc/postgresql/ssl/server.crt` |
| Key Path       | `/etc/postgresql/ssl/server.key` |
| Chain Path     | *(leave blank)*                  |
| Reload Command | `systemctl reload postgresql`    |

PostgreSQL requires the key file to be owned by the `postgres` user and mode `0600`. Run the connector as root (typical) and add an ownership step to the reload command:

```
chown postgres:postgres /etc/postgresql/ssl/server.key && chmod 600 /etc/postgresql/ssl/server.key && systemctl reload postgresql
```

In `postgresql.conf`:

```
ssl = on
ssl_cert_file = '/etc/postgresql/ssl/server.crt'
ssl_key_file  = '/etc/postgresql/ssl/server.key'
```

***

### MySQL / MariaDB

| Field          | Value                                                             |
| -------------- | ----------------------------------------------------------------- |
| Cert Path      | `/etc/mysql/ssl/server.crt`                                       |
| Key Path       | `/etc/mysql/ssl/server.key`                                       |
| Chain Path     | `/etc/mysql/ssl/ca.pem` *(optional — for client cert validation)* |
| Reload Command | `systemctl restart mysql`                                         |

MySQL requires a **restart** (not reload) to pick up new TLS certificates. In `my.cnf`:

```
[mysqld]
ssl_cert = /etc/mysql/ssl/server.crt
ssl_key  = /etc/mysql/ssl/server.key
```

For MariaDB, use `systemctl restart mariadb` instead.

***

### Envoy

| Field          | Value                                 |
| -------------- | ------------------------------------- |
| Cert Path      | `/etc/envoy/certs/cert.pem`           |
| Key Path       | `/etc/envoy/certs/key.pem`            |
| Chain Path     | *(leave blank)*                       |
| Reload Command | `kill -HUP $(cat /var/run/envoy.pid)` |

Alternatively, if running under systemd: `systemctl reload envoy`.

***

### Node.js / custom applications

For a Node.js HTTPS server or any custom application that reads cert files directly, use paths that match your application's configuration and a reload command appropriate for how your process manager restarts the app:

| Field          | Value                                            |
| -------------- | ------------------------------------------------ |
| Cert Path      | `/etc/myapp/ssl/cert.pem`                        |
| Key Path       | `/etc/myapp/ssl/key.pem`                         |
| Chain Path     | *(optional)*                                     |
| Reload Command | `systemctl restart myapp` or `pm2 restart myapp` |

If the application watches for file changes and reloads automatically, the reload command can be omitted.

## Managing App Connectors

From **Integrations → App Connectors** you can:

| Action               | Description                                                                                         |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| **Edit**             | Update paths, reload command, or Trust Profile — takes effect on next renewal                       |
| **Renew**            | Manually trigger a new issuance cycle, for example after a domain or path change                    |
| **Disable / Enable** | Pause cert delivery for an app without deleting it; disabled apps are excluded from connector polls |
| **Delete**           | Remove the app and its associated jobs                                                              |

## Renewal

CertForge schedules automatic renewal based on the cert expiry date and the renewal lead configured in the Domain Trust Policy (default 30 days). When the renewal window opens, CertForge queues a new `pending_csr` job and the connector picks it up on its next poll — no manual steps required.

To trigger early renewal at any time, click **Renew** on the app in the CertForge UI.

## How the private key is handled

The connector generates the RSA private key on the local host and writes it to `key_path` **before** submitting the CSR. This means:

* The key never leaves the host
* If the connector restarts between CSR submission and cert delivery, the key is already in place
* When using an ACME CA (Let's Encrypt, ZeroSSL), the CA generates its own key internally and returns it with the certificate — CertForge forwards this key to the connector, which writes it to `key_path`, ensuring the key on disk always matches the issued certificate

## Troubleshooting

**Job stuck in `pending_csr`** — the connector isn't running or can't reach CertForge. Check `journalctl -u certforge-connector` and verify the enrollment credentials are valid.

**Reload command failed** — the command exited non-zero. The error output is recorded in the job's **Error** field in CertForge. Common causes: wrong path, missing permissions, or the application isn't running. Fix the command under **Edit**, then click **Renew**.

**Key values mismatch on reload** — if you see `SSL_CTX_use_PrivateKey failed: key values mismatch` in your application logs, the key on disk doesn't match the cert. This can happen if the cert was written by a different process after the connector wrote the key. Click **Renew** to start a fresh issuance cycle — the connector will generate a new key and matching cert.

**App shows `error` status** — check the **Last Error** field on the App Connectors page for the failure message, and the connector log for more context.
