Skip to content

Commit 6c13564

Browse files
kingcdavidldez
andauthored
Adding S3 support for HTTP domain validation (#1970)
Co-authored-by: Fernandez Ludovic <[email protected]>
1 parent fc47c35 commit 6c13564

File tree

9 files changed

+244
-1
lines changed

9 files changed

+244
-1
lines changed

.golangci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,8 @@ issues:
181181
text: load is a global variable
182182
- path: 'providers/dns/([\d\w]+/)*[\d\w]+_test.go'
183183
text: 'envTest is a global variable'
184+
- path: 'providers/http/([\d\w]+/)*[\d\w]+_test.go'
185+
text: 'envTest is a global variable'
184186
- path: providers/dns/namecheap/namecheap_test.go
185187
text: 'testCases is a global variable'
186188
- path: providers/dns/acmedns/acmedns_test.go
@@ -222,4 +224,3 @@ issues:
222224
text: 'Duplicate words \(0\) found'
223225
- path: cmd/cmd_renew.go
224226
text: 'cyclomatic complexity 16 of func `renewForDomains` is high'
225-

cmd/flags.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ func CreateFlags(defaultPath string) []cli.Flag {
8787
Name: "http.memcached-host",
8888
Usage: "Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.",
8989
},
90+
&cli.StringFlag{
91+
Name: "http.s3-bucket",
92+
Usage: "Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.",
93+
},
9094
&cli.BoolFlag{
9195
Name: "tls",
9296
Usage: "Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges.",

cmd/setup_challenges.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"github.com/go-acme/lego/v4/log"
1414
"github.com/go-acme/lego/v4/providers/dns"
1515
"github.com/go-acme/lego/v4/providers/http/memcached"
16+
"github.com/go-acme/lego/v4/providers/http/s3"
1617
"github.com/go-acme/lego/v4/providers/http/webroot"
1718
"github.com/urfave/cli/v2"
1819
)
@@ -41,6 +42,7 @@ func setupChallenges(ctx *cli.Context, client *lego.Client) {
4142
}
4243
}
4344

45+
//nolint:gocyclo // the complexity is expected.
4446
func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
4547
switch {
4648
case ctx.IsSet("http.webroot"):
@@ -55,6 +57,12 @@ func setupHTTPProvider(ctx *cli.Context) challenge.Provider {
5557
log.Fatal(err)
5658
}
5759
return ps
60+
case ctx.IsSet("http.s3-bucket"):
61+
ps, err := s3.NewHTTPProvider(ctx.String("http.s3-bucket"))
62+
if err != nil {
63+
log.Fatal(err)
64+
}
65+
return ps
5866
case ctx.IsSet("http.port"):
5967
iface := ctx.String("http.port")
6068
if !strings.Contains(iface, ":") {

docs/data/zz_cli_help.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ GLOBAL OPTIONS:
3535
--http.proxy-header value Validate against this HTTP header when solving HTTP-01 based challenges behind a reverse proxy. (default: "Host")
3636
--http.webroot value Set the webroot folder to use for HTTP-01 based challenges to write directly to the .well-known/acme-challenge file. This disables the built-in server and expects the given directory to be publicly served with access to .well-known/acme-challenge
3737
--http.memcached-host value [ --http.memcached-host value ] Set the memcached host(s) to use for HTTP-01 based challenges. Challenges will be written to all specified hosts.
38+
--http.s3-bucket value Set the S3 bucket name to use for HTTP-01 based challenges. Challenges will be written to the S3 bucket.
3839
--tls Use the TLS-ALPN-01 challenge to solve challenges. Can be mixed with other types of challenges. (default: false)
3940
--tls.port value Set the port and interface to use for TLS-ALPN-01 based challenges to listen on. Supported: interface:port or :port. (default: ":443")
4041
--dns value Solve a DNS-01 challenge using the specified provider. Can be mixed with other types of challenges. Run 'lego dnshelp' for help on usage.

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/aws/aws-sdk-go-v2/credentials v1.13.27
2424
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2
2525
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4
26+
github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0
2627
github.com/aws/aws-sdk-go-v2/service/sts v1.19.3
2728
github.com/cenkalti/backoff/v4 v4.2.1
2829
github.com/civo/civogo v0.3.11
@@ -95,11 +96,16 @@ require (
9596
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
9697
github.com/AzureAD/microsoft-authentication-library-for-go v1.0.0 // indirect
9798
github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect
99+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect
98100
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.5 // indirect
99101
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.35 // indirect
100102
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 // indirect
101103
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 // indirect
104+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27 // indirect
105+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect
106+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30 // indirect
102107
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 // indirect
108+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4 // indirect
103109
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 // indirect
104110
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 // indirect
105111
github.com/aws/smithy-go v1.13.5 // indirect

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
7676
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
7777
github.com/aws/aws-sdk-go-v2 v1.19.0 h1:klAT+y3pGFBU/qVf1uzwttpBbiuozJYWzNLHioyDJ+k=
7878
github.com/aws/aws-sdk-go-v2 v1.19.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
79+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs=
80+
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno=
7981
github.com/aws/aws-sdk-go-v2/config v1.18.28 h1:TINEaKyh1Td64tqFvn09iYpKiWjmHYrG1fa91q2gnqw=
8082
github.com/aws/aws-sdk-go-v2/config v1.18.28/go.mod h1:nIL+4/8JdAuNHEjn/gPEXqtnS02Q3NXB/9Z7o5xE4+A=
8183
github.com/aws/aws-sdk-go-v2/credentials v1.13.27 h1:dz0yr/yR1jweAnsCx+BmjerUILVPQ6FS5AwF/OyG1kA=
@@ -88,12 +90,22 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29 h1:yOpYx+FTBdpk/g+sBU
8890
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.29/go.mod h1:M/eUABlDbw2uVrdAn+UsI6M727qp2fxkp8K0ejcBDUY=
8991
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36 h1:8r5m1BoAWkn0TDC34lUculryf7nUF25EgIMdjvGCkgo=
9092
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.36/go.mod h1:Rmw2M1hMVTwiUhjwMoIBFWFJMhvJbct06sSidxInkhY=
93+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27 h1:cZG7psLfqpkB6H+fIrgUDWmlzM474St1LP0jcz272yI=
94+
github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.27/go.mod h1:ZdjYvJpDlefgh8/hWelJhqgqJeodxu4SmbVsSdBlL7E=
95+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC6Zh2ojtV2LoC0MNwHWc6qXv/j2vrQtlftkdA=
96+
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8=
97+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30 h1:Bje8Xkh2OWpjBdNfXLrnn8eZg569dUQmhgtydxAYyP0=
98+
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.30/go.mod h1:qQtIBl5OVMfmeQkz8HaVyh5DzFmmFXyvK27UgIgOr4c=
9199
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29 h1:IiDolu/eLmuB18DRZibj77n1hHQT7z12jnGO7Ze3pLc=
92100
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.29/go.mod h1:fDbkK4o7fpPXWn8YAPmTieAMuB9mk/VgvW64uaUqxd4=
101+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4 h1:hx4WksB0NRQ9utR+2c3gEGzl6uKj3eM6PMQ6tN3lgXs=
102+
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.14.4/go.mod h1:JniVpqvw90sVjNqanGLufrVapWySL28fhBlYgl96Q/w=
93103
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2 h1:PwNeYoonBzmTdCztKiiutws3U24KrnDBuabzRfIlZY4=
94104
github.com/aws/aws-sdk-go-v2/service/lightsail v1.27.2/go.mod h1:gQhLZrTEath4zik5ixIe6axvgY5jJrgSBDJ360Fxnco=
95105
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4 h1:p4mTxJfCAyiTT4Wp6p/mOPa6j5MqCSRGot8qZwFs+Z0=
96106
github.com/aws/aws-sdk-go-v2/service/route53 v1.28.4/go.mod h1:VBLWpaHvhQNeu7N9rMEf00SWeOONb/HvaDUxe/7b44k=
107+
github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0 h1:PalLOEGZ/4XfQxpGZFTLaoJSmPoybnqJYotaIZEf/Rg=
108+
github.com/aws/aws-sdk-go-v2/service/s3 v1.37.0/go.mod h1:PwyKKVL0cNkC37QwLcrhyeCrAk+5bY8O2ou7USyAS2A=
97109
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13 h1:sWDv7cMITPcZ21QdreULwxOOAmE05JjEsT6fCDtDA9k=
98110
github.com/aws/aws-sdk-go-v2/service/sso v1.12.13/go.mod h1:DfX0sWuT46KpcqbMhJ9QWtxAIP1VozkDWf8VAkByjYY=
99111
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.13 h1:BFubHS/xN5bjl818QaroN6mQdjneYQ+AOx44KNXlyH4=

providers/http/s3/s3.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Package s3 implements a HTTP provider for solving the HTTP-01 challenge using web server's root path.
2+
package s3
3+
4+
import (
5+
"bytes"
6+
"context"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/config"
12+
"github.com/aws/aws-sdk-go-v2/service/s3"
13+
"github.com/go-acme/lego/v4/challenge/http01"
14+
)
15+
16+
// HTTPProvider implements ChallengeProvider for `http-01` challenge.
17+
type HTTPProvider struct {
18+
bucket string
19+
client *s3.Client
20+
}
21+
22+
// NewHTTPProvider returns a HTTPProvider instance with a configured s3 bucket and aws session.
23+
// Credentials must be passed in the environment variables.
24+
func NewHTTPProvider(bucket string) (*HTTPProvider, error) {
25+
if bucket == "" {
26+
return nil, fmt.Errorf("s3: bucket name missing")
27+
}
28+
29+
ctx := context.Background()
30+
31+
cfg, err := config.LoadDefaultConfig(ctx)
32+
if err != nil {
33+
return nil, fmt.Errorf("s3: unable to create AWS config: %w", err)
34+
}
35+
36+
client := s3.NewFromConfig(cfg)
37+
38+
return &HTTPProvider{
39+
bucket: bucket,
40+
client: client,
41+
}, nil
42+
}
43+
44+
// Present makes the token available at `HTTP01ChallengePath(token)` by creating a file in the given s3 bucket.
45+
func (s *HTTPProvider) Present(domain, token, keyAuth string) error {
46+
ctx := context.Background()
47+
48+
params := &s3.PutObjectInput{
49+
ACL: "public-read",
50+
Bucket: aws.String(s.bucket),
51+
Key: aws.String(strings.Trim(http01.ChallengePath(token), "/")),
52+
Body: bytes.NewReader([]byte(keyAuth)),
53+
}
54+
55+
_, err := s.client.PutObject(ctx, params)
56+
if err != nil {
57+
return fmt.Errorf("s3: failed to upload token to s3: %w", err)
58+
}
59+
return nil
60+
}
61+
62+
// CleanUp removes the file created for the challenge.
63+
func (s *HTTPProvider) CleanUp(domain, token, keyAuth string) error {
64+
ctx := context.Background()
65+
66+
params := &s3.DeleteObjectInput{
67+
Bucket: aws.String(s.bucket),
68+
Key: aws.String(strings.Trim(http01.ChallengePath(token), "/")),
69+
}
70+
71+
_, err := s.client.DeleteObject(ctx, params)
72+
if err != nil {
73+
return fmt.Errorf("s3: could not remove file in s3 bucket after HTTP challenge: %w", err)
74+
}
75+
76+
return nil
77+
}

providers/http/s3/s3.toml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
Name = "Amazon S3"
2+
Description = ''''''
3+
URL = "https://aws.amazon.com/s3/"
4+
Code = "s3"
5+
Since = "v4.14.0"
6+
7+
Example = '''
8+
AWS_ACCESS_KEY_ID=your_key_id \
9+
AWS_SECRET_ACCESS_KEY=your_secret_access_key \
10+
AWS_REGION=aws-region \
11+
lego --domains example.com --email [email protected] --http --http.s3-bucket your_s3_bucket --accept-tos=true run
12+
'''
13+
14+
Additional = '''
15+
## Description
16+
17+
AWS Credentials are automatically detected in the following locations and prioritized in the following order:
18+
19+
1. Environment variables: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, [`AWS_SESSION_TOKEN`]
20+
2. Shared credentials file (defaults to `~/.aws/credentials`, profiles can be specified using `AWS_PROFILE`)
21+
3. Amazon EC2 IAM role
22+
23+
The AWS Region is automatically detected in the following locations and prioritized in the following order:
24+
25+
1. Environment variables: `AWS_REGION`
26+
2. Shared configuration file if `AWS_SDK_LOAD_CONFIG` is set (defaults to `~/.aws/config`, profiles can be specified using `AWS_PROFILE`)
27+
28+
See also: https://aws.github.io/aws-sdk-go-v2/docs/configuring-sdk/
29+
30+
### Broad privileges for testing purposes
31+
32+
Will need to create an S3 bucket which has read permissions set for Everyone (public access).
33+
The S3 bucket doesn't require static website hosting to be enabled.
34+
AWS_REGION must match the region where the s3 bucket is hosted.
35+
'''
36+
37+
[Configuration]
38+
[Configuration.Credentials]
39+
AWS_ACCESS_KEY_ID = "Managed by the AWS client. Access key ID (`AWS_ACCESS_KEY_ID_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)"
40+
AWS_SECRET_ACCESS_KEY = "Managed by the AWS client. Secret access key (`AWS_SECRET_ACCESS_KEY_FILE` is not supported, use `AWS_SHARED_CREDENTIALS_FILE` instead)"
41+
AWS_REGION = "Managed by the AWS client (`AWS_REGION_FILE` is not supported)"
42+
S3_BUCKET = "Name of the s3 bucket"
43+
AWS_PROFILE = "Managed by the AWS client (`AWS_PROFILE_FILE` is not supported)"
44+
AWS_SDK_LOAD_CONFIG = "Managed by the AWS client. Retrieve the region from the CLI config file (`AWS_SDK_LOAD_CONFIG_FILE` is not supported)"
45+
AWS_ASSUME_ROLE_ARN = "Managed by the AWS Role ARN (`AWS_ASSUME_ROLE_ARN_FILE` is not supported)"
46+
AWS_EXTERNAL_ID = "Managed by STS AssumeRole API operation (`AWS_EXTERNAL_ID_FILE` is not supported)"
47+
[Configuration.Additional]
48+
AWS_SHARED_CREDENTIALS_FILE = "Managed by the AWS client. Shared credentials file."
49+
AWS_MAX_RETRIES = "The number of maximum returns the service will use to make an individual API request"
50+
51+
[Links]
52+
API = "https://docs.aws.amazon.com/AmazonS3/latest/userguide//Welcome.html"
53+
GoClient = "https://docs.aws.amazon.com/sdk-for-go/"
54+

providers/http/s3/s3_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Package s3 implements a HTTP provider for solving the HTTP-01 challenge
2+
// using AWS S3 in combination with AWS CloudFront.
3+
package s3
4+
5+
import (
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"os"
10+
"testing"
11+
12+
"github.com/go-acme/lego/v4/challenge/http01"
13+
"github.com/go-acme/lego/v4/platform/tester"
14+
"github.com/stretchr/testify/assert"
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
const (
19+
domain = "example.com"
20+
token = "foo"
21+
keyAuth = "bar"
22+
)
23+
24+
var envTest = tester.NewEnvTest(
25+
"AWS_ACCESS_KEY_ID",
26+
"AWS_SECRET_ACCESS_KEY",
27+
"AWS_REGION",
28+
"S3_BUCKET")
29+
30+
func TestLiveNewHTTPProvider_Valid(t *testing.T) {
31+
if !envTest.IsLiveTest() {
32+
t.Skip("skipping live test")
33+
}
34+
35+
envTest.RestoreEnv()
36+
37+
_, err := NewHTTPProvider(envTest.GetValue("S3_BUCKET"))
38+
require.NoError(t, err)
39+
}
40+
41+
func TestLiveNewHTTPProvider(t *testing.T) {
42+
if !envTest.IsLiveTest() {
43+
t.Skip("skipping live test")
44+
}
45+
46+
envTest.RestoreEnv()
47+
48+
s3Bucket := os.Getenv("S3_BUCKET")
49+
50+
provider, err := NewHTTPProvider(s3Bucket)
51+
require.NoError(t, err)
52+
53+
// Present
54+
55+
err = provider.Present(domain, token, keyAuth)
56+
require.NoError(t, err)
57+
58+
chlgPath := fmt.Sprintf("http://%s.s3.%s.amazonaws.com%s",
59+
s3Bucket, envTest.GetValue("AWS_REGION"), http01.ChallengePath(token))
60+
61+
resp, err := http.Get(chlgPath)
62+
require.NoError(t, err)
63+
64+
defer func() { _ = resp.Body.Close() }()
65+
66+
data, err := io.ReadAll(resp.Body)
67+
require.NoError(t, err)
68+
69+
assert.Equal(t, []byte(keyAuth), data)
70+
71+
// CleanUp
72+
73+
err = provider.CleanUp(domain, token, keyAuth)
74+
require.NoError(t, err)
75+
76+
cleanupResp, err := http.Get(chlgPath)
77+
require.NoError(t, err)
78+
79+
assert.Equal(t, cleanupResp.StatusCode, 403)
80+
}

0 commit comments

Comments
 (0)