Lewati ke konten utama
Versi: Berikutnya

Certificates and ACME API

All certificate routes require an authenticated session, global certificate permission, and the CSRF header for their POST writes. A custom proxy-host scope does not authorize certificate operations.

Route table

MethodPathPermissionSuccessKey failures
GET/api/certificatescertificates.read200 CertificateMetadata[]401, 403, 500
POST/api/certificatescertificates.write201 CertificateUploadResponse400, 401, 403, 409, 413
POST/api/certificates/{id}/activatecertificates.write204400, 401, 403, 404, 502 reload_failed
POST/api/certificates/acmecertificates.write202 AcmeJobResponse`400 invalid_hostname
POST/api/certificates/{id}/renewcertificates.write202 AcmeJobResponse401, 403, 404 not_found, 409 acme_busy, 502 acme_failed
GET/api/certificates/{id}/statuscertificates.read200 AcmeStatus401, 403, 404 not_found, 500 acme_failed

204 No Content from activation has no response JSON. Successful upload, activation, issue, and renewal notification flows publish certificates.changed to the authenticated event stream.

Certificate list and upload

Certificate list entries contain:

{
"id": 12,
"name": "test-cert",
"source": "custom",
"covered_hostnames": ["app.example.test"],
"expiry": "Jul 18 00:00:00 2027 GMT",
"active": false
}

These six fields are the complete certificate-list response. ACME state is available only from GET /api/certificates/{id}/status. Upload uses multipart/form-data, not JSON. Supply these exact field names:

Multipart fieldRequiredValue
nameYesCertificate label as UTF-8 text.
certificateYesPEM certificate bytes.
keyYesPEM private-key bytes.

The total multipart data may not exceed 3 MiB and each individual field may not exceed 1 MiB; excess data returns 413 Payload Too Large. Missing fields, invalid multipart data, or invalid certificate/key material returns 400. A persistence conflict returns 409. On 201, the response deliberately omits active:

{
"id": 12,
"name": "test-cert",
"source": "custom",
"covered_hostnames": ["app.example.test"],
"expiry": "Jul 18 00:00:00 2027 GMT"
}

Use files that exist only on the local test machine. Never paste PEM data into a command line or store a private key in a cookie jar.

curl --fail-with-body -k -b "$cookie_jar" \
-H "X-CSRF-Token: $csrf_token" \
-F 'name=test-cert' \
-F 'certificate=@./testdata/cert.pem;type=application/x-pem-file' \
-F 'key=@./testdata/key.pem;type=application/x-pem-file' \
https://127.0.0.1:8081/api/certificates

Activation

POST /api/certificates/{id}/activate validates the stored certificate paths, makes that certificate active, and reloads TLS state. An unknown ID is 404; a record with invalid material is 400; a failed runtime reload is 502 with reload_failed. On a reload failure, the handler restores the prior active certificate state before returning the failure.

curl --fail-with-body -k -b "$cookie_jar" \
-H "X-CSRF-Token: $csrf_token" \
-X POST https://127.0.0.1:8081/api/certificates/12/activate

ACME issue and renewal

POST /api/certificates/acme accepts this JSON shape:

{
"environment": "staging",
"challenge": "http01",
"hostnames": ["app.example.test"],
"cloudflare_token": "test-only-token"
}

environment is exactly staging or production. BeaRust selects the corresponding Let's Encrypt directory and keeps account keys namespaced by environment; use staging to test an integration and switch intentionally to production for a trusted hostname. challenge is exactly http01 or cloudflare_dns01.

For http01, BeaRust serves the HTTP-01 value only at the exact /.well-known/acme-challenge/{token} path. Wildcard hostnames are rejected with 400 unsupported_challenge. For cloudflare_dns01, send a cloudflare_token; cloudflare_api_token is accepted as an input alias for compatibility. The token is write-only: it is not serialized in a job, certificate, status, audit record, or API error. The service zeroes its local request bytes after use.

Hostnames are trimmed, lowercased, deduplicated, and validated. Empty values, whitespace, malformed DNS names, malformed wildcards, or no remaining hostname return 400 invalid_hostname.

An accepted issue or renewal responds while its job is running:

{"job_id":"test-job-id","certificate_id":12}

The status is 202 Accepted, not a guarantee that issuance has completed. Only one ACME operation may run at a time; contention is 409 acme_busy. Provider/service failures are 502 acme_failed. Renewal additionally returns 404 not_found when the certificate has no ACME status.

Status response and safe failure handling

GET /api/certificates/{id}/status returns:

{
"certificate_id": 12,
"environment": "staging",
"challenge": "http01",
"hostnames": ["app.example.test"],
"renewal_state": "issued",
"next_renewal_at": "2027-06-18T00:00:00Z",
"last_attempt_at": "2026-08-17T00:00:00Z",
"last_error_code": null
}

The two timestamps and last_error_code can be null. Use the error code as the safe renewal-failure signal; do not expect provider responses, account keys, certificates, key paths, private keys, or Cloudflare credentials in a status or error response. 404 not_found means no ACME status exists for that certificate; 500 acme_failed means status lookup was unavailable.

Source pointers

The HTTP handlers and responses are in bearust/src/control_plane/mod.rs and bearust/src/control_plane/models.rs. ACME input processing and the write-only token boundary are in bearust/src/certificates/acme_service.rs; directory and environment behavior are in bearust/src/acme/client.rs. Relevant integration tests include tests/acme_http01.rs, tests/acme_dns01.rs, tests/certificate_renewal.rs, and tests/certificates.rs.