Skip to content

Commit 80ae435

Browse files
authored
pebble-challtestsrv: small binary for mock DNS & ACME challenges. (#181)
Boulder has a nice handy [`challtestsrv` package and command](https://github.com/letsencrypt/boulder/tree/9e39680e3f78c410e2d780a7badfe200a31698eb/test/challtestsrv) used for integration tests. Its small and useful enough that the library portion has been promoted to a first-class repo: https://github.com/letsencrypt/challtestsrv The stand-alone binary with an HTTP management interface can come live in the Pebble repo where more folks can use it without pulling in all of Boulder. I've heard from a few ACME client developers that this would be useful to them. It is possible we could achieve the same thing by leaving the binary in the Boulder repo using the updated code that doesn't import other things from Boulder. Moving it out of the repo will help us commit to working on abstractions that make tests cleaner. This also makes it quick and easy to have a full Pebble environment with mock DNS without needing to install tools from other repos. The dependency on the letsencrypt/challtestsrv package does require pulling in a dep. on `github.com/miekg/dns` (and vendoring it) but I think its a fair tradeoff. The provided Dockerfile is now split into two dockerfiles (see `docker/` directory): one for `pebble` and one for `pebble-challtestsrv`. They are both updated to use Go 1.11, to build with the vendored modules instead of fetching them at build time, and to use the latest Alpine base image. A new `docker-compose.yml` example is included that starts up a `pebble-challtestsrv` container and a `pebble` container that uses the former as its DNS server. The README is updated to explain the usage briefly.
1 parent f07faa4 commit 80ae435

File tree

589 files changed

+229462
-13
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

589 files changed

+229462
-13
lines changed

.dockerignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
11
.git
22
pebble.exe
33
pebble
4-
vendor/

README.md

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,38 @@ pebble -config ./test/config/pebble-config.json
8282
Afterwards you can access the Pebble server's ACME directory
8383
at `https://localhost:14000/dir`.
8484

85-
### Docker Image
85+
### Docker
86+
87+
Pebble includes a [docker-compose](https://docs.docker.com/compose/) file that
88+
will create a `pebble` instance that uses a `pebble-challtestsrv` instance for
89+
DNS resolution.
90+
91+
To build and start the containers run:
92+
93+
```
94+
docker-compose up
95+
```
96+
97+
Afterwards you can access the ACME API from your host machine at
98+
`https://localhost:14000/dir` and the `pebble-challtestsrv`'s management
99+
interface at `http://locahost:8055`.
100+
101+
To get started you may want to update the `pebble-challtestsrv` mock DNS data
102+
with a new default IPv4 address to use to respond to `A` queries from `pebble`:
103+
104+
```
105+
curl --request POST --data '{"ip":"172.20.0.1"}' http://localhost:8055/set-default-ipv4
106+
```
107+
108+
See the [pebble-challtestsrv
109+
README](https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md)
110+
for more information.
111+
112+
#### Prebuilt Docker Images
113+
114+
If you would prefer not to use the provided `docker-compose.yml`, or to build
115+
container images yourself, you can also use the [published
116+
images](https://hub.docker.com/r/letsencrypt/pebble/).
86117

87118
With a docker-compose file:
88119

@@ -92,7 +123,7 @@ version: '3'
92123
services:
93124
pebble:
94125
image: letsencrypt/pebble
95-
command: pebble -config ./test/my-pebble-config.json
126+
command: pebble -config /test/my-pebble-config.json
96127
ports:
97128
- 14000:14000
98129
environment:
@@ -135,14 +166,21 @@ By default Pebble uses the system DNS resolver, this may mean that caching cause
135166
problems with DNS-01 validation. It may also mean that no DNSSEC validation is
136167
performed.
137168
You should configure your system's recursive DNS resolver according to your
138-
needs or use the `-dnsserver` flag to define an address to a DNS server.
169+
needs or use the `-dnsserver` flag to define an address to a DNS server.
139170

140171
```
141172
pebble -dnsserver 10.10.10.10:5053
142173
pebble -dnsserver 8.8.8.8:53
143174
pebble -dnsserver :5053
144175
```
145176

177+
You may find it useful to set `pebble`'s `-dnsserver` to the address you used as
178+
the `-dns01` argument when starting up a `pebble-challtestsrv` instance. This
179+
will let you easily mock DNS data for Pebble. See the included
180+
`docker-compose.yml` and the [pebble-challtestsrv
181+
README](https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md)
182+
for more information.
183+
146184
### Testing at full speed
147185

148186
By default Pebble will sleep a random number of seconds (from 0 to 15) between

cmd/config.go renamed to cmd/command.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"encoding/json"
55
"fmt"
66
"io/ioutil"
7+
"log"
78
"os"
9+
"os/signal"
10+
"syscall"
811
)
912

1013
// ReadConfigFile takes a file path as an argument and attempts to
@@ -31,3 +34,28 @@ func FailOnError(err error, msg string) {
3134
os.Exit(1)
3235
}
3336
}
37+
38+
var signalToName = map[os.Signal]string{
39+
syscall.SIGTERM: "SIGTERM",
40+
syscall.SIGINT: "SIGINT",
41+
syscall.SIGHUP: "SIGHUP",
42+
}
43+
44+
// CatchSignals catches SIGTERM, SIGINT, SIGHUP and executes a callback
45+
// method before exiting
46+
func CatchSignals(callback func()) {
47+
sigChan := make(chan os.Signal, 1)
48+
signal.Notify(sigChan, syscall.SIGTERM)
49+
signal.Notify(sigChan, syscall.SIGINT)
50+
signal.Notify(sigChan, syscall.SIGHUP)
51+
52+
sig := <-sigChan
53+
log.Printf("Caught %s", signalToName[sig])
54+
55+
if callback != nil {
56+
callback()
57+
}
58+
59+
log.Printf("Exiting")
60+
os.Exit(0)
61+
}

cmd/pebble-challtestsrv/README.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# Pebble Challenge Test Server
2+
3+
**Important note: The `pebble-challtestsrv` command is for TEST USAGE ONLY. It
4+
is trivially insecure, offering no authentication. Only use
5+
`pebble-challtestsrv` in a controlled test environment.**
6+
7+
The standalone `pebble-challtestsrv` binary lets you run HTTP-01, HTTPS HTTP-01,
8+
DNS-01, and TLS-ALPN-01 challenge servers that external programs can add/remove
9+
challenge responses to using a HTTP management API.
10+
11+
For example this is used by the Boulder integration tests to easily add/remove
12+
TXT records for DNS-01 challenges for the `chisel.py` ACME client, and to test
13+
redirect behaviour for HTTP-01 challenge validation.
14+
15+
### Usage
16+
17+
```
18+
Usage of pebble-challtestsrv:
19+
-dns01 string
20+
Comma separated bind addresses/ports for DNS-01 challenges and fake DNS data. Set empty to disable. (default ":8053")
21+
-http01 string
22+
Comma separated bind addresses/ports for HTTP-01 challenges. Set empty to disable. (default ":5002")
23+
-https01 string
24+
Comma separated bind addresses/ports for HTTPS HTTP-01 challenges. Set empty to disable. (default ":5003")
25+
-management string
26+
Bind address/port for management HTTP interface (default ":8055")
27+
-tlsalpn01 string
28+
Comma separated bind addresses/ports for TLS-ALPN-01 and HTTPS HTTP-01 challenges. Set empty to disable. (default ":5001")
29+
```
30+
31+
To disable a challenge type, set the bind address to `""`. E.g.:
32+
33+
* To run HTTP-01 only: `pebble-challtestsrv -dns01 "" -tlsalpn01 ""`
34+
* To run DNS-01 only: `challtestsrv -http01 "" -tlsalpn01 ""`
35+
* To run TLS-ALPN-01 only: `challtestsrv -http01 "" -dns01 ""`
36+
37+
### Management Interface
38+
39+
_Note: These examples assume the default `-management` interface address, `:8056`._
40+
41+
#### Mock DNS
42+
43+
##### Default A/AAAA Responses
44+
45+
To set the default IPv4 address used for responses to `A` queries that do not
46+
match explicit mocks run:
47+
48+
curl -X POST -d '{"ip":"10.10.10.2"}' http://localhost:8056/set-default-ipv4
49+
50+
Similarly to set the default IPv6 address used for responses to `AAAA` queries
51+
that do not match explicit mocks run:
52+
53+
curl -X POST -d '{"ip":"::1"}' http://localhost:8056/set-default-ipv6
54+
55+
To clear the default IPv4 or IPv6 address POST the same endpoints with an empty
56+
(`""`) IP.
57+
58+
##### Mocked A/AAAA Responses
59+
60+
To add IPv4 addresses to be returned for `A` queries for
61+
`test-host.letsencrypt.org` run:
62+
63+
curl -X POST -d '{"host":"test-host.letsencrypt.org", "addresses":["12.12.12.12", "13.13.13.13"]}' http://localhost:8056/add-a
64+
65+
The mocked `A` responses can be removed by running:
66+
67+
curl -X POST -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8056/clear-a
68+
69+
To add IPv6 addresses to be returned for `AAAA` queries for
70+
`test-host.letsencrypt.org` run:
71+
72+
curl -X POST -d '{"host":"test-host.letsencrypt.org", "addresses":["2001:4860:4860::8888", "2001:4860:4860::8844"]}' http://localhost:8056/add-aaaa
73+
74+
The mocked `AAAA` responses can be removed by running:
75+
76+
curl -X POST -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8056/clear-aaaa
77+
78+
##### Mocked CAA Responses
79+
80+
To add a mocked CAA policy for `test-host.letsencrypt.org` that allows issuance
81+
by `letsencrypt.org` run:
82+
83+
curl -X POST -d '{"host":"test-host.letsencrypt.org", "policies":[{"tag":"issue","value":"letsencrypt.org"}]}' http://localhost:8055/add-caa
84+
85+
To remove the mocked CAA policy for `test-host.letsencrypt.org` run:
86+
87+
curl -X POST -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8055/clear-caa
88+
89+
#### HTTP-01
90+
91+
To add an HTTP-01 challenge response for the token `"aaaa"` with the content `"bbbb"` run:
92+
93+
curl -X POST -d '{"token":"aaaa", "content":"bbbb"}' http://localhost:8056/add-http01
94+
95+
Afterwards the challenge response will be available over HTTP at
96+
`http://localhost:5002/.well-known/acme-challenge/aaaa`, and HTTPS at
97+
`https://localhost:5002/.well-known/acme-challenge/aaaa`.
98+
99+
The HTTP-01 challenge response for the `"aaaa"` token can be deleted by running:
100+
101+
curl -X POST -d '{"token":"aaaa"}' http://localhost:8056/del-http01
102+
103+
##### Redirects
104+
105+
To add a redirect from `/.well-known/acme-challenge/whatever` to
106+
`https://localhost:5003/ok` run:
107+
108+
curl -X POST -d '{"path":"/.well-known/whatever", "targetURL": "https://localhost:5003/ok"}' http://localhost:8056/add-redirect
109+
110+
Afterwards HTTP requests to `http://localhost:5002/.well-known/whatever/` will
111+
be redirected to `https://localhost:5003/ok`. HTTPS requests that match the
112+
path will not be served a redirect to prevent loops when redirecting the same
113+
path from HTTP to HTTPS.
114+
115+
To remove the redirect run:
116+
117+
curl -X POST -d '{"path":"/.well-known/whatever"}' http://localhost:8056/del-redirect
118+
119+
#### DNS-01
120+
121+
To add a DNS-01 challenge response for `_acme-challenge.test-host.letsencrypt.org` with
122+
the value `"foo"` run:
123+
124+
curl -X POST -d '{"host":"_acme-challenge.test-host.letsencrypt.org", "value": "foo"}' http://localhost:8056/add-txt
125+
126+
To remove the mocked DNS-01 challenge response run:
127+
128+
curl -X POST -d '{"host":"_acme-challenge.test-host.letsencrypt.org"}' http://localhost:8056/clear-txt
129+
130+
#### TLS-ALPN-01
131+
132+
To add a TLS-ALPN-01 challenge response certificate for the host
133+
`test-host.letsencrypt.org` with the key authorization `"foo"` run:
134+
135+
curl -X POST -d '{"host":"test-host.letsencrypt.org", "content":"foo"}' http://localhost:8056/add-tlsalpn01
136+
137+
To remove the mocked TLS-ALPN-01 challenge response run:
138+
139+
curl -X POST -d '{"host":"test-host.letsencrypt.org"}' http://localhost:8056/clear-tlsalpn01

cmd/pebble-challtestsrv/dnsone.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package main
2+
3+
import "net/http"
4+
5+
// addDNS01 handles an HTTP POST request to add a new DNS-01 challenge TXT
6+
// record for a given host/value.
7+
//
8+
// The POST body is expected to have two non-empty parameters:
9+
// "host" - the hostname to add the mock TXT response under.
10+
// "value" - the key authorization value to return in the TXT response.
11+
//
12+
// A successful POST will write http.StatusOK to the client.
13+
func (srv *managementServer) addDNS01(w http.ResponseWriter, r *http.Request) {
14+
// Unmarshal the request body JSON as a request object
15+
var request struct {
16+
Host string
17+
Value string
18+
}
19+
if err := mustParsePOST(&request, r); err != nil {
20+
http.Error(w, err.Error(), http.StatusBadRequest)
21+
return
22+
}
23+
24+
// If the request has an empty host or value it's a bad request
25+
if request.Host == "" || request.Value == "" {
26+
w.WriteHeader(http.StatusBadRequest)
27+
return
28+
}
29+
30+
// Add the DNS-01 challenge response TXT to the challenge server
31+
srv.challSrv.AddDNSOneChallenge(request.Host, request.Value)
32+
srv.log.Printf("Added DNS-01 TXT challenge for Host %q - Value %q\n",
33+
request.Host, request.Value)
34+
w.WriteHeader(http.StatusOK)
35+
}
36+
37+
// delDNS01 handles an HTTP POST request to delete an existing DNS-01 challenge
38+
// TXT record for a given host.
39+
//
40+
// The POST body is expected to have one non-empty parameter:
41+
// "host" - the hostname to remove the mock TXT response for.
42+
//
43+
// A successful POST will write http.StatusOK to the client.
44+
func (srv *managementServer) delDNS01(w http.ResponseWriter, r *http.Request) {
45+
// Unmarshal the request body JSON as a request object
46+
var request struct {
47+
Host string
48+
}
49+
if err := mustParsePOST(&request, r); err != nil {
50+
http.Error(w, err.Error(), http.StatusBadRequest)
51+
return
52+
}
53+
54+
// If the request has an empty host value it's a bad request
55+
if request.Host == "" {
56+
w.WriteHeader(http.StatusBadRequest)
57+
return
58+
}
59+
60+
// Delete the DNS-01 challenge response TXT for the given host from the
61+
// challenge server
62+
srv.challSrv.DeleteDNSOneChallenge(request.Host)
63+
srv.log.Printf("Removed DNS-01 TXT challenge for Host %q\n", request.Host)
64+
w.WriteHeader(http.StatusOK)
65+
}

cmd/pebble-challtestsrv/http.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"io/ioutil"
7+
"net/http"
8+
)
9+
10+
// mustParsePOST will attempt to read a JSON POST body from the provided request
11+
// and unmarshal it into the provided ob. If an error occurs at any point it
12+
// will be returned.
13+
func mustParsePOST(ob interface{}, request *http.Request) error {
14+
jsonBody, err := ioutil.ReadAll(request.Body)
15+
if err != nil {
16+
return err
17+
}
18+
19+
if string(jsonBody) == "" {
20+
return errors.New("Expected JSON POST body, was empty")
21+
}
22+
23+
return json.Unmarshal(jsonBody, ob)
24+
}

0 commit comments

Comments
 (0)