aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddytls/ondemand.go
blob: 0970234cecdbeeef9134aaa4befd7438fcad60ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
// Copyright 2015 Matthew Holt and The Caddy Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package caddytls

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"net/http"
	"net/url"
	"time"

	"github.com/caddyserver/certmagic"
	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"

	"github.com/caddyserver/caddy/v2"
	"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
)

func init() {
	caddy.RegisterModule(PermissionByHTTP{})
}

// OnDemandConfig configures on-demand TLS, for obtaining
// needed certificates at handshake-time. Because this
// feature can easily be abused, Caddy must ask permission
// to your application whether a particular domain is allowed
// to have a certificate issued for it.
type OnDemandConfig struct {
	// Deprecated. WILL BE REMOVED SOON. Use 'permission' instead with the `http` module.
	Ask string `json:"ask,omitempty"`

	// REQUIRED. A module that will determine whether a
	// certificate is allowed to be loaded from storage
	// or obtained from an issuer on demand.
	PermissionRaw json.RawMessage `json:"permission,omitempty" caddy:"namespace=tls.permission inline_key=module"`
	permission    OnDemandPermission
}

// OnDemandPermission is a type that can give permission for
// whether a certificate should be allowed to be obtained or
// loaded from storage on-demand.
// EXPERIMENTAL: This API is experimental and subject to change.
type OnDemandPermission interface {
	// CertificateAllowed returns nil if a certificate for the given
	// name is allowed to be either obtained from an issuer or loaded
	// from storage on-demand.
	//
	// The context passed in has the associated *tls.ClientHelloInfo
	// value available at the certmagic.ClientHelloInfoCtxKey key.
	//
	// In the worst case, this function may be called as frequently
	// as every TLS handshake, so it should return as quick as possible
	// to reduce latency. In the normal case, this function is only
	// called when a certificate is needed that is not already loaded
	// into memory ready to serve.
	CertificateAllowed(ctx context.Context, name string) error
}

// PermissionByHTTP determines permission for a TLS certificate by
// making a request to an HTTP endpoint.
type PermissionByHTTP struct {
	// The endpoint to access. It should be a full URL.
	// A query string parameter "domain" will be added to it,
	// containing the domain (or IP) for the desired certificate,
	// like so: `?domain=example.com`. Generally, this endpoint
	// is not exposed publicly to avoid a minor information leak
	// (which domains are serviced by your application).
	//
	// The endpoint must return a 200 OK status if a certificate
	// is allowed; anything else will cause it to be denied.
	// Redirects are not followed.
	Endpoint string `json:"endpoint"`

	logger   *zap.Logger
	replacer *caddy.Replacer
}

// CaddyModule returns the Caddy module information.
func (PermissionByHTTP) CaddyModule() caddy.ModuleInfo {
	return caddy.ModuleInfo{
		ID:  "tls.permission.http",
		New: func() caddy.Module { return new(PermissionByHTTP) },
	}
}

// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
func (p *PermissionByHTTP) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
	if !d.Next() {
		return nil
	}
	if !d.AllArgs(&p.Endpoint) {
		return d.ArgErr()
	}
	return nil
}

func (p *PermissionByHTTP) Provision(ctx caddy.Context) error {
	p.logger = ctx.Logger()
	p.replacer = caddy.NewReplacer()
	return nil
}

func (p PermissionByHTTP) CertificateAllowed(ctx context.Context, name string) error {
	// run replacer on endpoint URL (for environment variables) -- return errors to prevent surprises (#5036)
	askEndpoint, err := p.replacer.ReplaceOrErr(p.Endpoint, true, true)
	if err != nil {
		return fmt.Errorf("preparing 'ask' endpoint: %v", err)
	}

	askURL, err := url.Parse(askEndpoint)
	if err != nil {
		return fmt.Errorf("parsing ask URL: %v", err)
	}
	qs := askURL.Query()
	qs.Set("domain", name)
	askURL.RawQuery = qs.Encode()
	askURLString := askURL.String()

	var remote string
	if chi, ok := ctx.Value(certmagic.ClientHelloInfoCtxKey).(*tls.ClientHelloInfo); ok && chi != nil {
		remote = chi.Conn.RemoteAddr().String()
	}

	if c := p.logger.Check(zapcore.DebugLevel, "asking permission endpoint"); c != nil {
		c.Write(
			zap.String("remote", remote),
			zap.String("domain", name),
			zap.String("url", askURLString),
		)
	}

	resp, err := onDemandAskClient.Get(askURLString)
	if err != nil {
		return fmt.Errorf("checking %v to determine if certificate for hostname '%s' should be allowed: %v",
			askEndpoint, name, err)
	}
	resp.Body.Close()

	if c := p.logger.Check(zapcore.DebugLevel, "response from permission endpoint"); c != nil {
		c.Write(
			zap.String("remote", remote),
			zap.String("domain", name),
			zap.String("url", askURLString),
			zap.Int("status", resp.StatusCode),
		)
	}

	if resp.StatusCode < 200 || resp.StatusCode > 299 {
		return fmt.Errorf("%s: %w %s - non-2xx status code %d", name, ErrPermissionDenied, askEndpoint, resp.StatusCode)
	}

	return nil
}

// ErrPermissionDenied is an error that should be wrapped or returned when the
// configured permission module does not allow a certificate to be issued,
// to distinguish that from other errors such as connection failure.
var ErrPermissionDenied = errors.New("certificate not allowed by permission module")

// These perpetual values are used for on-demand TLS.
var (
	onDemandAskClient = &http.Client{
		Timeout: 10 * time.Second,
		CheckRedirect: func(req *http.Request, via []*http.Request) error {
			return fmt.Errorf("following http redirects is not allowed")
		},
	}
)

// Interface guards
var (
	_ OnDemandPermission = (*PermissionByHTTP)(nil)
	_ caddy.Provisioner  = (*PermissionByHTTP)(nil)
)