Skip to content

Commit db84370

Browse files
accuserstgraber
authored andcommitted
incusd/acme: Use lego for HTTP-01
Signed-off-by: Matthew Gibbons <[email protected]>
1 parent c9d3717 commit db84370

File tree

6 files changed

+85
-272
lines changed

6 files changed

+85
-272
lines changed

cmd/incusd/acme.go cmd/incusd/api_acme.go

+38-12
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ package main
22

33
import (
44
"context"
5+
"io"
6+
"net"
57
"net/http"
6-
"net/url"
7-
8-
"github.com/gorilla/mux"
8+
"strings"
99

1010
"github.com/lxc/incus/v6/internal/server/acme"
1111
"github.com/lxc/incus/v6/internal/server/cluster"
@@ -32,11 +32,7 @@ var acmeChallengeCmd = APIEndpoint{
3232
func acmeProvideChallenge(d *Daemon, r *http.Request) response.Response {
3333
s := d.State()
3434

35-
token, err := url.PathUnescape(mux.Vars(r)["token"])
36-
if err != nil {
37-
return response.SmartError(err)
38-
}
39-
35+
// Redirect to the leader when clustered.
4036
if s.ServerClustered {
4137
leader, err := s.Cluster.LeaderAddress()
4238
if err != nil {
@@ -57,14 +53,44 @@ func acmeProvideChallenge(d *Daemon, r *http.Request) response.Response {
5753
}
5854
}
5955

60-
if d.http01Provider == nil || d.http01Provider.Token() != token {
61-
return response.NotFound(nil)
56+
// Forward to the lego listener.
57+
addr := s.GlobalConfig.ACMEHTTP()
58+
if strings.HasPrefix(addr, ":") {
59+
addr = "127.0.0.1" + addr
60+
}
61+
62+
domain, _, _, _, _ := s.GlobalConfig.ACME()
63+
64+
client := http.Client{}
65+
client.Transport = &http.Transport{
66+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
67+
return net.Dial("tcp", addr)
68+
},
69+
}
70+
71+
req, err := http.NewRequest("GET", "http://"+domain+r.URL.String(), nil)
72+
if err != nil {
73+
return response.InternalError(err)
74+
}
75+
76+
req.Header = r.Header
77+
78+
resp, err := client.Do(req)
79+
if err != nil {
80+
return response.InternalError(err)
81+
}
82+
83+
defer resp.Body.Close()
84+
85+
challenge, err := io.ReadAll(resp.Body)
86+
if err != nil {
87+
return response.InternalError(err)
6288
}
6389

6490
return response.ManualResponse(func(w http.ResponseWriter) error {
6591
w.Header().Set("Content-Type", "text/plain")
6692

67-
_, err := w.Write([]byte(d.http01Provider.KeyAuth()))
93+
_, err = w.Write(challenge)
6894
if err != nil {
6995
return err
7096
}
@@ -98,7 +124,7 @@ func autoRenewCertificate(ctx context.Context, d *Daemon, force bool) error {
98124
}
99125

100126
opRun := func(op *operations.Operation) error {
101-
newCert, err := acme.UpdateCertificate(s, challengeType, d.http01Provider, s.ServerClustered, domain, email, caURL, force)
127+
newCert, err := acme.UpdateCertificate(s, challengeType, s.ServerClustered, domain, email, caURL, force)
102128
if err != nil {
103129
return err
104130
}

cmd/incusd/daemon.go

-5
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ import (
3030
internalIO "github.com/lxc/incus/v6/internal/io"
3131
"github.com/lxc/incus/v6/internal/linux"
3232
"github.com/lxc/incus/v6/internal/rsync"
33-
"github.com/lxc/incus/v6/internal/server/acme"
3433
"github.com/lxc/incus/v6/internal/server/apparmor"
3534
"github.com/lxc/incus/v6/internal/server/auth"
3635
"github.com/lxc/incus/v6/internal/server/auth/oidc"
@@ -156,9 +155,6 @@ type Daemon struct {
156155

157156
lokiClient *loki.Client
158157

159-
// HTTP-01 challenge provider for ACME
160-
http01Provider acme.HTTP01Provider
161-
162158
// Authorization.
163159
authorizer auth.Authorizer
164160

@@ -198,7 +194,6 @@ func newDaemon(config *DaemonConfig, os *sys.OS) *Daemon {
198194
devIncusEvents: devIncusEvents,
199195
events: incusEvents,
200196
db: &db.DB{},
201-
http01Provider: acme.NewHTTP01Provider(),
202197
os: os,
203198
setupChan: make(chan struct{}),
204199
waitReady: cancel.New(context.Background()),

internal/server/acme/acme.go

+47-132
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,13 @@ package acme
22

33
import (
44
"context"
5-
"crypto/ecdsa"
6-
"crypto/elliptic"
7-
"crypto/rand"
85
"crypto/tls"
96
"crypto/x509"
107
"fmt"
118
"os"
129
"slices"
1310
"time"
1411

15-
"github.com/go-acme/lego/v4/acme"
16-
"github.com/go-acme/lego/v4/certcrypto"
17-
"github.com/go-acme/lego/v4/certificate"
18-
"github.com/go-acme/lego/v4/lego"
19-
"github.com/go-acme/lego/v4/registration"
20-
2112
"github.com/lxc/incus/v6/internal/server/state"
2213
internalUtil "github.com/lxc/incus/v6/internal/util"
2314
"github.com/lxc/incus/v6/shared/logger"
@@ -36,14 +27,20 @@ const retries = 5
3627
// certificate at a later stage.
3728
const ClusterCertFilename = "cluster.crt.new"
3829

30+
// CertKeyPair describes a certificate and its private key.
31+
type CertKeyPair struct {
32+
Certificate []byte `json:"-"`
33+
PrivateKey []byte `json:"-"`
34+
}
35+
3936
// certificateNeedsUpdate returns true if the domain doesn't match the certificate's DNS names
4037
// or it's valid for less than 30 days.
4138
func certificateNeedsUpdate(domain string, cert *x509.Certificate) bool {
4239
return !slices.Contains(cert.DNSNames, domain) || time.Now().After(cert.NotAfter.Add(-30*24*time.Hour))
4340
}
4441

4542
// UpdateCertificate updates the certificate.
46-
func UpdateCertificate(s *state.State, challengeType string, provider ChallengeProvider, clustered bool, domain string, email string, caURL string, force bool) (*certificate.Resource, error) {
43+
func UpdateCertificate(s *state.State, challengeType string, clustered bool, domain string, email string, caURL string, force bool) (*CertKeyPair, error) {
4744
clusterCertFilename := internalUtil.VarPath(ClusterCertFilename)
4845

4946
l := logger.AddContext(logger.Ctx{"domain": domain, "caURL": caURL, "challenge": challengeType})
@@ -75,7 +72,7 @@ func UpdateCertificate(s *state.State, challengeType string, provider ChallengeP
7572
}
7673

7774
if !certificateNeedsUpdate(domain, cert) {
78-
return &certificate.Resource{
75+
return &CertKeyPair{
7976
Certificate: clusterCert,
8077
PrivateKey: key,
8178
}, nil
@@ -102,148 +99,66 @@ func UpdateCertificate(s *state.State, challengeType string, provider ChallengeP
10299
return nil, nil
103100
}
104101

102+
tmpDir, err := os.MkdirTemp("", "lego")
103+
if err != nil {
104+
return nil, fmt.Errorf("Failed to create temporary directory: %w", err)
105+
}
106+
107+
defer func() {
108+
err := os.RemoveAll(tmpDir)
109+
if err != nil {
110+
logger.Warn("Failed to remove temporary directory", logger.Ctx{"err": err})
111+
}
112+
}()
113+
114+
env := os.Environ()
115+
116+
args := []string{
117+
"--accept-tos",
118+
"--domains", domain,
119+
"--email", email,
120+
"--path", tmpDir,
121+
"--server", caURL,
122+
}
123+
105124
if challengeType == "DNS-01" {
106125
provider, environment, resolvers := s.GlobalConfig.ACMEDNS()
107126

127+
env = append(env, environment...)
128+
108129
if provider == "" {
109130
return nil, fmt.Errorf("DNS-01 challenge type requires acme.dns.provider configuration key to be set")
110131
}
111132

112-
tmpDir, err := os.MkdirTemp("", "lego")
113-
if err != nil {
114-
return nil, fmt.Errorf("Failed to create temporary directory: %w", err)
115-
}
116-
117-
defer func() {
118-
err := os.RemoveAll(tmpDir)
119-
if err != nil {
120-
logger.Warn("Failed to remove temporary directory", logger.Ctx{"err": err})
121-
}
122-
}()
123-
124-
args := []string{
125-
"--accept-tos",
126-
"--dns", provider,
127-
"--domains", domain,
128-
"--email", email,
129-
"--path", tmpDir,
130-
"--server", caURL,
131-
}
132-
133+
args = append(args, "--dns", provider)
133134
if len(resolvers) > 0 {
134135
for _, resolver := range resolvers {
135136
args = append(args, "--dns.resolvers", resolver)
136137
}
137138
}
139+
} else if challengeType == "HTTP-01" {
140+
args = append(args, "--http")
138141

139-
args = append(args, "run")
140-
141-
_, _, err = subprocess.RunCommandSplit(context.TODO(), append(os.Environ(), environment...), nil, "lego", args...)
142-
if err != nil {
143-
return nil, fmt.Errorf("Failed to run lego command: %w", err)
142+
port := s.GlobalConfig.ACMEHTTP()
143+
if port != "" {
144+
args = append(args, "--http.port", port)
144145
}
145-
146-
certInfo, err = localtls.KeyPairAndCA(tmpDir+"/certificates", domain, localtls.CertServer, true)
147-
if err != nil {
148-
return nil, fmt.Errorf("Failed to load certificate and key file: %w", err)
149-
}
150-
151-
return &certificate.Resource{
152-
Certificate: certInfo.PublicKey(),
153-
PrivateKey: certInfo.PrivateKey(),
154-
}, nil
155-
}
156-
157-
// Generate new private key for user. This key needs to be different from the server's private key.
158-
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
159-
if err != nil {
160-
return nil, fmt.Errorf("Failed generating private key for user account: %w", err)
161-
}
162-
163-
user := user{
164-
Email: email,
165-
Key: privateKey,
166146
}
167147

168-
config := lego.NewConfig(&user)
148+
args = append(args, "run")
169149

170-
if caURL != "" {
171-
config.CADirURL = caURL
172-
} else {
173-
// Default URL for Let's Encrypt
174-
config.CADirURL = "https://acme-v02.api.letsencrypt.org/directory"
175-
}
176-
177-
config.Certificate.KeyType = certcrypto.RSA2048
178-
179-
client, err := lego.NewClient(config)
150+
_, _, err = subprocess.RunCommandSplit(context.TODO(), env, nil, "lego", args...)
180151
if err != nil {
181-
return nil, fmt.Errorf("Failed to create new client: %w", err)
152+
return nil, fmt.Errorf("Failed to run lego command: %w", err)
182153
}
183154

184-
err = provider.RegisterWithSolver(client.Challenge)
155+
certInfo, err = localtls.KeyPairAndCA(tmpDir+"/certificates", domain, localtls.CertServer, true)
185156
if err != nil {
186-
return nil, fmt.Errorf("Failed setting challenge provider: %w", err)
187-
}
188-
189-
var reg *registration.Resource
190-
191-
// Registration might fail randomly (as seen in manual tests), so retry in that case.
192-
for i := 0; i < retries; i++ {
193-
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
194-
if err == nil {
195-
break
196-
}
197-
198-
// In case we were rate limited, don't try again.
199-
details, ok := err.(*acme.ProblemDetails)
200-
if ok && details.Type == "urn:ietf:params:acme:error:rateLimited" {
201-
break
202-
}
203-
204-
l.Warn("Failed to register user, retrying in 10 seconds", logger.Ctx{"err": err})
205-
time.Sleep(10 * time.Second)
206-
}
207-
208-
if err != nil {
209-
return nil, fmt.Errorf("Failed to register user: %w", err)
210-
}
211-
212-
user.Registration = reg
213-
214-
request := certificate.ObtainRequest{
215-
Domains: []string{domain},
216-
Bundle: true,
217-
PrivateKey: certInfo.KeyPair().PrivateKey,
218-
}
219-
220-
var certificates *certificate.Resource
221-
222-
l.Info("Issuing certificate")
223-
224-
// Get new certificate.
225-
// This might fail randomly (as seen in manual tests), so retry in that case.
226-
for i := 0; i < retries; i++ {
227-
certificates, err = client.Certificate.Obtain(request)
228-
if err == nil {
229-
break
230-
}
231-
232-
// In case we were rate limited, don't try again.
233-
details, ok := err.(*acme.ProblemDetails)
234-
if ok && details.Type == "urn:ietf:params:acme:error:rateLimited" {
235-
break
236-
}
237-
238-
l.Warn("Failed to obtain certificate, retrying in 10 seconds", logger.Ctx{"err": err})
239-
time.Sleep(10 * time.Second)
240-
}
241-
242-
if err != nil {
243-
return nil, fmt.Errorf("Failed to obtain certificate: %w", err)
157+
return nil, fmt.Errorf("Failed to load certificate and key file: %w", err)
244158
}
245159

246-
l.Info("Finished issuing certificate")
247-
248-
return certificates, nil
160+
return &CertKeyPair{
161+
Certificate: certInfo.PublicKey(),
162+
PrivateKey: certInfo.PrivateKey(),
163+
}, nil
249164
}

internal/server/acme/http-01-provider.go

-24
This file was deleted.

0 commit comments

Comments
 (0)