Hey everyone, Alex here. Welcome back to another edition of Coding with Alex at sysseder.com.
If you are developing an ACME client or testing certificate automation, Pebble provides a small local test server. ACME means Automatic Certificate Management Environment. Pebble comes from the Let’s Encrypt project, is intentionally unsuitable as a production CA, and differs from Boulder in some behavior. It does not enforce production rate limits and can insert artificial validation delays.
The earlier article framed these capabilities as a “July 2026 Mega Update” that topped Hacker News. The official release material checked for this revision does not substantiate that framing. This guide instead examines documented testing features and makes no claim about that ranking or a newly released multi-perspective simulator.
We will distinguish Pebble from its separate challenge-response server, examine configuration and show a small Go management client. The examples are illustrative: they do not constitute a complete certificate-issuing client, a compliance assessment or a tested production deployment.
Why Pebble Matters to Modern Developers
Local HTTPS can be awkward: developers may create certificates with openssl, configure trust or disable verification. In Node.js, rejectUnauthorized: false disables an important check; it is not a generic Python setting. Prefer properly scoped test-client trust. The bundled HTTPS test CA is pebble.minica.pem; its private key is public. Never add that CA to a system-wide trust store or production environment.
In an ACME-enabled production deployment, clients such as Certbot or Lego and integrations such as Caddy or Traefik manage certificate requests and renewals. Exercising the protocol locally can reveal some routing, challenge-response and renewal bugs earlier, but cannot reproduce every production condition.
Pebble exposes an ACME directory locally, normally at https://localhost:14000/dir. Clients can test orders, challenge validation and renewal behavior against it. The test CA does not persist its state across runs, and successful local issuance is not evidence that all public-CA validation rules or rate-limit handling will work.
Which Testing Capabilities Are Documented?
The useful building blocks include configurable challenge ports, a separate controllable DNS/challenge server, and strict-mode compatibility checks. Keep these distinct from geographically distributed validation and network-fault simulation.
1. Multiple DNS Addresses Are Not Multiple Geographic Perspectives
Let’s Encrypt uses multi-perspective validation to check domain control over multiple network paths. This makes some routing and DNS-path attacks harder; it does not guarantee that every such attack is prevented.
The companion pebble-challtestsrv can configure lists of A and AAAA responses. That supports controlled DNS test cases, but does not imply virtual geographic validators, packet-loss simulation or a requiredSuccesses quorum setting. The inspected Pebble configuration exposes none of those controls.
2. Managing DNS-01 Challenge Responses
Dynamic DNS responses are managed through the separate challenge server’s HTTP API, not a new DNS engine inside Pebble. For DNS-01, use /set-txt and /clear-txt on its management port. Pebble is configured to query that server with -dnsserver. The management API is unauthenticated and belongs only in an isolated test environment.
3. Strict Mode and Compatibility Testing
The -strict command-line flag enables upcoming compatibility-breaking API behavior. It is not an arbitrary error-injection controller or a promise of complete RFC coverage. DNS failure responses can be arranged separately in the challenge server; use only the controls documented for your selected release.
Setting Up an Isolated Pebble Test Lab
The docker-compose.yml example uses two services: Pebble and its challenge server. It does not include a lego client or a configured web server. An ACME client and its challenge-handling integration must be added separately for end-to-end issuance tests.
The Docker Compose Blueprint
Set PEBBLE_IMAGE and CHALLTESTSRV_IMAGE to verified digest-pinned images from the same selected release in the official GHCR repositories. The placeholders deliberately prevent an accidental unpinned launch. Check that the subnet does not conflict with your environment. Host ports bind to loopback, but this is still a test lab: do not share its unauthenticated management services. PEBBLE_VA_NOSLEEP disables artificial validation sleeps; omit it when testing delayed validation.
# Illustrative test lab. Set both variables to verified, matching-release image digests.
services:
pebble:
image: ${PEBBLE_IMAGE:?Set a verified ghcr.io/letsencrypt/pebble image digest}
command: ["-config", "/test/config/pebble-config.json", "-strict", "-dnsserver", "10.30.50.3:8053"]
ports:
- "127.0.0.1:14000:14000"
- "127.0.0.1:15000:15000"
volumes:
- ./pebble-config.json:/test/config/pebble-config.json:ro
environment:
PEBBLE_VA_NOSLEEP: "1"
networks:
acme-net:
ipv4_address: 10.30.50.2
challtestsrv:
image: ${CHALLTESTSRV_IMAGE:?Set a verified ghcr.io/letsencrypt/pebble-challtestsrv image digest}
command: ["-defaultIPv6", "", "-defaultIPv4", "10.30.50.3"]
ports:
- "127.0.0.1:8055:8055"
networks:
acme-net:
ipv4_address: 10.30.50.3
networks:
acme-net:
driver: bridge
ipam:
config:
- subnet: 10.30.50.0/24
Configuring Pebble (pebble-config.json)
Save the following as pebble-config.json only after checking it against the selected release. It supplies server certificate/key paths and challenge ports, with a three-day default certificate profile. The previously shown multiPerspectiveValidation and dnsToggles settings are not present in the inspected schema. Strict mode and resolver selection are command-line flags, not JSON fields.
{
"pebble": {
"listenAddress": "0.0.0.0:14000",
"managementListenAddress": "0.0.0.0:15000",
"certificate": "/test/certs/localhost/cert.pem",
"privateKey": "/test/certs/localhost/key.pem",
"httpPort": 5002,
"tlsPort": 5001,
"profiles": {
"default": {
"description": "Three-day certificates for an isolated test lab",
"validityPeriod": 259200
}
}
}
}
validityPeriod is expressed in seconds; 259,200 seconds is exactly three days. Short lifetimes can help exercise renewal logic but do not guarantee production behavior. As of this revision, Let’s Encrypt has announced a 64-day default production lifetime from February 10, 2027, and offers shorter profiles. Test renewal scheduling rather than hardcoding an assumption that every certificate lasts 90 days.
Programmatic DNS-01 Mocking with Go
This Go program manages one TXT response through pebble-challtestsrv at loopback HTTP port 8055. Set DNS01_VALUE to the DNS-01 value computed by your test ACME client, run the program to register it, perform the integration check separately, then run with -clear to remove it. An arbitrary token will not satisfy a real ACME challenge. The program has a timeout but neither requests a certificate nor configures TLS trust. A client calling Pebble’s HTTPS API must configure trust for the bundled HTTPS test CA, pebble.minica.pem, only in its own isolated client configuration.
// Local test-only management client; it does not request a certificate.
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"time"
)
func main() {
clear := flag.Bool("clear", false, "Remove the mock TXT record")
flag.Parse()
payload := map[string]string{
"host": "_acme-challenge.dev.example.test.",
}
path := "/set-txt"
if *clear {
path = "/clear-txt"
} else {
value := os.Getenv("DNS01_VALUE")
if value == "" {
log.Fatal("Set DNS01_VALUE to the test ACME client's DNS-01 value")
}
payload["value"] = value
}
body, err := json.Marshal(payload)
if err != nil {
log.Fatal(err)
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Post(
"http://127.0.0.1:8055"+path,
"application/json", bytes.NewReader(body),
)
if err != nil {
log.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatalf("Challenge server returned HTTP %d", resp.StatusCode)
}
fmt.Printf("Test DNS management request %s succeeded; no certificate was requested.\n", path)
}
Testing Failure Paths: Why Every Dev Needs This
A successful issuance test is only one path. Also consider timeouts, slow TXT propagation and failed challenge responses. Observe how the client reports errors and retries; do not infer correctness merely from a browser connection succeeding.
For a documented DNS failure case, the companion server supports /set-servfail and /clear-servfail for a chosen host. Use a dedicated test name, then check client errors and bounded retry/backoff behavior. This is not a simulated failure of geographic perspectives or a requiredSuccesses quorum. Pebble does not enforce Let’s Encrypt’s production rate limits, so those require separate testing.
Conclusion & Next Steps
Pebble and its companion challenge server can make local ACME integration tests more useful. They provide controlled challenge responses and compatibility checks, not complete production equivalence or built-in geographic network simulation. Keeping those boundaries clear is part of designing a trustworthy test suite.
Choose the tool for the test: manual development certificates or mkcert may address a different need from exercising an ACME client. If you add Pebble, isolate its services, keep verification enabled with client-scoped trust, select matching versions and add explicit assertions for failures and renewals. No local test alone establishes application security or compliance.
Let’s Hear From You!
Are you using ACME locally, or are you still relying on manual certs or local tools like mkcert? Have you ever had your automated TLS certificates break in production due to a validation error? Let's talk about it in the comments below!
Until next time, keep your connections encrypted and your code clean. See you in the next post!
Primary references for the revisions
- Pebble documentation and test-only limitations
- Challenge server management API
- Pebble configuration and command-line schema
- Upstream example configuration
- Upstream two-service Compose architecture
- Official release history
- Multi-perspective validation at Let’s Encrypt
- Certificate-lifetime announcement
- RFC 8555 ACME