3 Commits
v4.3 ... v4.4

Author SHA1 Message Date
Felipe Leopoldo Sologuren Gutiérrez
c286b3b1d0 Added TokenBucket to limit the rate of validation failures (#151)
Added new TockenBucket named `acmeClientFailLimit` to avoid being banned because of the [Failed validation limit](https://letsencrypt.org/docs/failed-validation-limit/) of Let's Encrypt.

The behaviour is similar to the other limiters blocking the `obtainCert` func ensuring rate under limit.

Co-authored-by: fsologureng <sologuren@estudiohum.cl>
Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/151
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Felipe Leopoldo Sologuren Gutiérrez <fsologureng@noreply.codeberg.org>
Co-committed-by: Felipe Leopoldo Sologuren Gutiérrez <fsologureng@noreply.codeberg.org>
2023-01-04 05:26:14 +00:00
6543
f7fad2a5ae Integration Tests use https://codeberg.org/cb_pages_tests 2023-01-04 06:08:06 +01:00
Gusted
98d198d419 Safely get certificate's leaf (#150)
- It's not guaranteed that `tls.X509KeyPair` will set `c.Leaf`.
- This patch fixes this by using a wrapper that parses the leaf
certificate(in bytes) if `c.Leaf` wasn't set.
- Resolves #149

Co-authored-by: Gusted <postmaster@gusted.xyz>
Reviewed-on: https://codeberg.org/Codeberg/pages-server/pulls/150
Reviewed-by: 6543 <6543@obermui.de>
Co-authored-by: Gusted <gusted@noreply.codeberg.org>
Co-committed-by: Gusted <gusted@noreply.codeberg.org>
2023-01-04 04:51:27 +00:00
2 changed files with 29 additions and 7 deletions

View File

@@ -31,7 +31,7 @@ func TestGetRedirect(t *testing.T) {
func TestGetContent(t *testing.T) { func TestGetContent(t *testing.T) {
log.Println("=== TestGetContent ===") log.Println("=== TestGetContent ===")
// test get image // test get image
resp, err := getTestHTTPSClient().Get("https://magiclike.localhost.mock.directory:4430/images/827679288a.jpg") resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/images/827679288a.jpg")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
t.FailNow() t.FailNow()
@@ -42,7 +42,7 @@ func TestGetContent(t *testing.T) {
assert.Len(t, resp.Header.Get("ETag"), 42) assert.Len(t, resp.Header.Get("ETag"), 42)
// specify branch // specify branch
resp, err = getTestHTTPSClient().Get("https://momar.localhost.mock.directory:4430/pag/@master/") resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pag/@master/")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.NotNil(t, resp) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
@@ -53,7 +53,7 @@ func TestGetContent(t *testing.T) {
assert.Len(t, resp.Header.Get("ETag"), 44) assert.Len(t, resp.Header.Get("ETag"), 44)
// access branch name contains '/' // access branch name contains '/'
resp, err = getTestHTTPSClient().Get("https://blumia.localhost.mock.directory:4430/pages-server-integration-tests/@docs~main/") resp, err = getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/blumia/@docs~main/")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) { if !assert.EqualValues(t, http.StatusOK, resp.StatusCode) {
t.FailNow() t.FailNow()
@@ -81,7 +81,7 @@ func TestCustomDomain(t *testing.T) {
func TestGetNotFound(t *testing.T) { func TestGetNotFound(t *testing.T) {
log.Println("=== TestGetNotFound ===") log.Println("=== TestGetNotFound ===")
// test custom not found pages // test custom not found pages
resp, err := getTestHTTPSClient().Get("https://crystal.localhost.mock.directory:4430/pages-404-demo/blah") resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/pages-404-demo/blah")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.NotNil(t, resp) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
@@ -95,7 +95,7 @@ func TestGetNotFound(t *testing.T) {
func TestFollowSymlink(t *testing.T) { func TestFollowSymlink(t *testing.T) {
log.Printf("=== TestFollowSymlink ===\n") log.Printf("=== TestFollowSymlink ===\n")
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/link") resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/link")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.NotNil(t, resp) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()
@@ -111,7 +111,7 @@ func TestFollowSymlink(t *testing.T) {
func TestLFSSupport(t *testing.T) { func TestLFSSupport(t *testing.T) {
log.Printf("=== TestLFSSupport ===\n") log.Printf("=== TestLFSSupport ===\n")
resp, err := getTestHTTPSClient().Get("https://6543.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt") resp, err := getTestHTTPSClient().Get("https://cb_pages_tests.localhost.mock.directory:4430/tests_for_pages-server/@main/lfs.txt")
assert.NoError(t, err) assert.NoError(t, err)
if !assert.NotNil(t, resp) { if !assert.NotNil(t, resp) {
t.FailNow() t.FailNow()

View File

@@ -163,6 +163,9 @@ var acmeClientOrderLimit = equalizer.NewTokenBucket(25, 15*time.Minute)
// rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests) // rate limit is 20 / second, we want 5 / second (especially as one cert takes at least two requests)
var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second) var acmeClientRequestLimit = equalizer.NewTokenBucket(5, 1*time.Second)
// rate limit is 5 / hour https://letsencrypt.org/docs/failed-validation-limit/
var acmeClientFailLimit = equalizer.NewTokenBucket(5, 1*time.Hour)
type AcmeTLSChallengeProvider struct { type AcmeTLSChallengeProvider struct {
challengeCache cache.SetGetKey challengeCache cache.SetGetKey
} }
@@ -278,6 +281,9 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
res, err = acmeClient.Certificate.Renew(*renew, true, false, "") res, err = acmeClient.Certificate.Renew(*renew, true, false, "")
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains) log.Error().Err(err).Msgf("Couldn't renew certificate for %v, trying to request a new one", domains)
if acmeUseRateLimits {
acmeClientFailLimit.Take()
}
res = nil res = nil
} }
} }
@@ -298,12 +304,19 @@ func obtainCert(acmeClient *lego.Client, domains []string, renew *certificate.Re
Bundle: true, Bundle: true,
MustStaple: false, MustStaple: false,
}) })
if acmeUseRateLimits && err != nil {
acmeClientFailLimit.Take()
}
} }
if err != nil { if err != nil {
log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains) log.Error().Err(err).Msgf("Couldn't obtain again a certificate or %v", domains)
if renew != nil && renew.CertURL != "" { if renew != nil && renew.CertURL != "" {
tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey) tlsCertificate, err := tls.X509KeyPair(renew.Certificate, renew.PrivateKey)
if err == nil && tlsCertificate.Leaf.NotAfter.After(time.Now()) { if err != nil {
return mockCert(domains[0], err.Error(), mainDomainSuffix, keyDatabase), err
}
leaf, err := leaf(&tlsCertificate)
if err == nil && leaf.NotAfter.After(time.Now()) {
// avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at // avoid sending a mock cert instead of a still valid cert, instead abuse CSR field to store time to try again at
renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10)) renew.CSR = []byte(strconv.FormatInt(time.Now().Add(6*time.Hour).Unix(), 10))
if err := keyDatabase.Put(name, renew); err != nil { if err := keyDatabase.Put(name, renew); err != nil {
@@ -529,3 +542,12 @@ func MaintainCertDB(ctx context.Context, interval time.Duration, mainDomainSuffi
} }
} }
} }
// leaf returns the parsed leaf certificate, either from c.leaf or by parsing
// the corresponding c.Certificate[0].
func leaf(c *tls.Certificate) (*x509.Certificate, error) {
if c.Leaf != nil {
return c.Leaf, nil
}
return x509.ParseCertificate(c.Certificate[0])
}