mirror of
https://github.com/TecharoHQ/anubis.git
synced 2026-04-23 08:36:41 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b72ded756 | |||
| 439860443a | |||
| 33bc4669cb | |||
| fa5ceedf23 | |||
| 49dcdbc49e | |||
| 578f248ce1 | |||
| 298fc0b847 | |||
| 8af9845117 |
@@ -38,4 +38,3 @@ Samsung
|
|||||||
wenet
|
wenet
|
||||||
qwertiko
|
qwertiko
|
||||||
setuplistener
|
setuplistener
|
||||||
mba
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ cachediptoasn
|
|||||||
Caddyfile
|
Caddyfile
|
||||||
caninetools
|
caninetools
|
||||||
Cardyb
|
Cardyb
|
||||||
CAs
|
|
||||||
celchecker
|
celchecker
|
||||||
celphase
|
celphase
|
||||||
cerr
|
cerr
|
||||||
@@ -204,10 +203,8 @@ kagi
|
|||||||
kagibot
|
kagibot
|
||||||
Keyfunc
|
Keyfunc
|
||||||
keypair
|
keypair
|
||||||
keypairreloader
|
|
||||||
KHTML
|
KHTML
|
||||||
kinda
|
kinda
|
||||||
kpr
|
|
||||||
KUBECONFIG
|
KUBECONFIG
|
||||||
lcj
|
lcj
|
||||||
ldflags
|
ldflags
|
||||||
@@ -232,7 +229,6 @@ metarefresh
|
|||||||
metrix
|
metrix
|
||||||
mimi
|
mimi
|
||||||
Minfilia
|
Minfilia
|
||||||
minica
|
|
||||||
mistralai
|
mistralai
|
||||||
mnt
|
mnt
|
||||||
Mojeek
|
Mojeek
|
||||||
@@ -317,7 +313,6 @@ searchbot
|
|||||||
searx
|
searx
|
||||||
sebest
|
sebest
|
||||||
secretplans
|
secretplans
|
||||||
selfsigned
|
|
||||||
Semrush
|
Semrush
|
||||||
Seo
|
Seo
|
||||||
setsebool
|
setsebool
|
||||||
|
|||||||
@@ -174,27 +174,6 @@ status_codes:
|
|||||||
# metrics:
|
# metrics:
|
||||||
# bind: ":9090"
|
# bind: ":9090"
|
||||||
# network: "tcp"
|
# network: "tcp"
|
||||||
#
|
|
||||||
# # To protect your metrics server with basic auth, set credentials below:
|
|
||||||
# #
|
|
||||||
# # https://anubis.techaro.lol/docs/admin/policies#http-basic-authentication
|
|
||||||
# basicAuth:
|
|
||||||
# username: ""
|
|
||||||
# password: ""
|
|
||||||
#
|
|
||||||
# # To serve metrics over TLS, set the path to the right TLS certificate and key
|
|
||||||
# # here. When the files change on disk, they will automatically be reloaded.
|
|
||||||
# #
|
|
||||||
# # https://anubis.techaro.lol/docs/admin/policies#tls
|
|
||||||
# tls:
|
|
||||||
# certificate: /path/to/tls.crt
|
|
||||||
# key: /path/to/tls.key
|
|
||||||
#
|
|
||||||
# # If you want to secure your metrics endpoint using mutual TLS (mTLS), set
|
|
||||||
# # the path to a certificate authority public certificate here.
|
|
||||||
# #
|
|
||||||
# # https://anubis.techaro.lol/docs/admin/policies#mtls
|
|
||||||
# ca: /path/to/ca.crt
|
|
||||||
|
|
||||||
# Anubis can store temporary data in one of a few backends. See the storage
|
# Anubis can store temporary data in one of a few backends. See the storage
|
||||||
# backends section of the docs for more information:
|
# backends section of the docs for more information:
|
||||||
|
|||||||
@@ -4,5 +4,5 @@
|
|||||||
# - Claude-SearchBot: No published IP allowlist
|
# - Claude-SearchBot: No published IP allowlist
|
||||||
- name: "ai-crawlers-search"
|
- name: "ai-crawlers-search"
|
||||||
user_agent_regex: >-
|
user_agent_regex: >-
|
||||||
OAI-SearchBot|Claude-SearchBot|PerplexityBot|meta-webindexer
|
OAI-SearchBot|Claude-SearchBot|PerplexityBot
|
||||||
action: DENY
|
action: DENY
|
||||||
|
|||||||
@@ -21,8 +21,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
|
- Improve error messages and fix broken REDIRECT_DOMAINS link in docs ([#1193](https://github.com/TecharoHQ/anubis/issues/1193))
|
||||||
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
|
- Add Bulgarian locale ([#1394](https://github.com/TecharoHQ/anubis/pull/1394))
|
||||||
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
|
- Fix CEL internal errors when iterating `headers`/`query` map wrappers by implementing map iterators for `HTTPHeaders` and `URLValues` ([#1465](https://github.com/TecharoHQ/anubis/pull/1465)).
|
||||||
- Enable [metrics serving via TLS](./admin/policies.mdx#tls), including [mutual TLS (mTLS)](./admin/policies.mdx#mtls).
|
|
||||||
- Enable [HTTP basic auth](./admin/policies.mdx#http-basic-authentication) for the metrics server.
|
|
||||||
|
|
||||||
## v1.25.0: Necron
|
## v1.25.0: Necron
|
||||||
|
|
||||||
|
|||||||
@@ -138,57 +138,6 @@ metrics:
|
|||||||
socketMode: "0700" # must be a string
|
socketMode: "0700" # must be a string
|
||||||
```
|
```
|
||||||
|
|
||||||
### TLS
|
|
||||||
|
|
||||||
If you want to serve the metrics server over TLS, use the `tls` block:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
metrics:
|
|
||||||
bind: ":9090"
|
|
||||||
network: "tcp"
|
|
||||||
|
|
||||||
tls:
|
|
||||||
certificate: /path/to/tls.crt
|
|
||||||
key: /path/to/tls.key
|
|
||||||
```
|
|
||||||
|
|
||||||
The certificate and key will automatically be reloaded when the respective files change.
|
|
||||||
|
|
||||||
### mTLS
|
|
||||||
|
|
||||||
If you want to validate requests to ensure that they use a client certificate signed by a certificate authority (mutual TLS or mTLS), set the `ca` value in the `tls` block:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
metrics:
|
|
||||||
bind: ":9090"
|
|
||||||
network: "tcp"
|
|
||||||
|
|
||||||
tls:
|
|
||||||
certificate: /path/to/tls.crt
|
|
||||||
key: /path/to/tls.key
|
|
||||||
ca: /path/to/ca.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
As it is not expected for certificate authority certificates to change often, the CA certificate will NOT be automatically reloaded when the respective file changes.
|
|
||||||
|
|
||||||
### HTTP basic authentication
|
|
||||||
|
|
||||||
Anubis' metrics server also supports setting HTTP basic auth as a lightweight protection against unauthorized users viewing metrics data. As the basic auth credentials are hardcoded in the configuration file, administrators SHOULD use randomly generated credentials, such as type-4 UUIDs or other high entropy strings. These credentials MUST NOT be sensitive or used to protect other high value systems.
|
|
||||||
|
|
||||||
Configure it with the `basicAuth` block under `metrics`:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
metrics:
|
|
||||||
bind: ":9090"
|
|
||||||
network: "tcp"
|
|
||||||
|
|
||||||
basicAuth:
|
|
||||||
username: azurediamond
|
|
||||||
password: hunter2
|
|
||||||
```
|
|
||||||
|
|
||||||
If you have Python installed, you can generate a high entropy password with `python -c 'import secrets; print(secrets.token_urlsafe(32))'`.
|
|
||||||
|
|
||||||
## Imprint / Impressum support
|
## Imprint / Impressum support
|
||||||
|
|
||||||
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
Anubis has support for showing imprint / impressum information. This is defined in the `impressum` block of your configuration. See [Imprint / Impressum configuration](./configuration/impressum.mdx) for more information.
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"crypto/subtle"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
// BasicAuth wraps next in HTTP Basic authentication using the provided
|
|
||||||
// credentials. If either username or password is empty, next is returned
|
|
||||||
// unchanged and a debug log line is emitted.
|
|
||||||
//
|
|
||||||
// Credentials are compared in constant time to avoid leaking information
|
|
||||||
// through timing side channels.
|
|
||||||
func BasicAuth(realm, username, password string, next http.Handler) http.Handler {
|
|
||||||
if username == "" || password == "" {
|
|
||||||
slog.Debug("skipping middleware, basic auth credentials are empty")
|
|
||||||
return next
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedUser := sha256.Sum256([]byte(username))
|
|
||||||
expectedPass := sha256.Sum256([]byte(password))
|
|
||||||
challenge := fmt.Sprintf("Basic realm=%q, charset=\"UTF-8\"", realm)
|
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
user, pass, ok := r.BasicAuth()
|
|
||||||
if !ok {
|
|
||||||
unauthorized(w, challenge)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
gotUser := sha256.Sum256([]byte(user))
|
|
||||||
gotPass := sha256.Sum256([]byte(pass))
|
|
||||||
|
|
||||||
userMatch := subtle.ConstantTimeCompare(gotUser[:], expectedUser[:])
|
|
||||||
passMatch := subtle.ConstantTimeCompare(gotPass[:], expectedPass[:])
|
|
||||||
|
|
||||||
if userMatch&passMatch != 1 {
|
|
||||||
unauthorized(w, challenge)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
next.ServeHTTP(w, r)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func unauthorized(w http.ResponseWriter, challenge string) {
|
|
||||||
w.Header().Set("WWW-Authenticate", challenge)
|
|
||||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
package internal
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func okHandler() http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write([]byte("ok"))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasicAuth(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
const (
|
|
||||||
realm = "test-realm"
|
|
||||||
username = "admin"
|
|
||||||
password = "hunter2"
|
|
||||||
)
|
|
||||||
|
|
||||||
for _, tt := range []struct {
|
|
||||||
name string
|
|
||||||
setAuth bool
|
|
||||||
user string
|
|
||||||
pass string
|
|
||||||
wantStatus int
|
|
||||||
wantBody string
|
|
||||||
wantChall bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid credentials",
|
|
||||||
setAuth: true,
|
|
||||||
user: username,
|
|
||||||
pass: password,
|
|
||||||
wantStatus: http.StatusOK,
|
|
||||||
wantBody: "ok",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing credentials",
|
|
||||||
setAuth: false,
|
|
||||||
wantStatus: http.StatusUnauthorized,
|
|
||||||
wantChall: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrong username",
|
|
||||||
setAuth: true,
|
|
||||||
user: "nobody",
|
|
||||||
pass: password,
|
|
||||||
wantStatus: http.StatusUnauthorized,
|
|
||||||
wantChall: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrong password",
|
|
||||||
setAuth: true,
|
|
||||||
user: username,
|
|
||||||
pass: "wrong",
|
|
||||||
wantStatus: http.StatusUnauthorized,
|
|
||||||
wantChall: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty supplied credentials",
|
|
||||||
setAuth: true,
|
|
||||||
user: "",
|
|
||||||
pass: "",
|
|
||||||
wantStatus: http.StatusUnauthorized,
|
|
||||||
wantChall: true,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
h := BasicAuth(realm, username, password, okHandler())
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
if tt.setAuth {
|
|
||||||
req.SetBasicAuth(tt.user, tt.pass)
|
|
||||||
}
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != tt.wantStatus {
|
|
||||||
t.Errorf("status = %d, want %d", rec.Code, tt.wantStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
if tt.wantBody != "" && rec.Body.String() != tt.wantBody {
|
|
||||||
t.Errorf("body = %q, want %q", rec.Body.String(), tt.wantBody)
|
|
||||||
}
|
|
||||||
|
|
||||||
chall := rec.Header().Get("WWW-Authenticate")
|
|
||||||
if tt.wantChall {
|
|
||||||
if chall == "" {
|
|
||||||
t.Error("WWW-Authenticate header missing on 401")
|
|
||||||
}
|
|
||||||
if !strings.Contains(chall, realm) {
|
|
||||||
t.Errorf("WWW-Authenticate = %q, want realm %q", chall, realm)
|
|
||||||
}
|
|
||||||
} else if chall != "" {
|
|
||||||
t.Errorf("unexpected WWW-Authenticate header: %q", chall)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBasicAuthPassthrough(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tt := range []struct {
|
|
||||||
name string
|
|
||||||
username string
|
|
||||||
password string
|
|
||||||
}{
|
|
||||||
{name: "empty username", username: "", password: "hunter2"},
|
|
||||||
{name: "empty password", username: "admin", password: ""},
|
|
||||||
{name: "both empty", username: "", password: ""},
|
|
||||||
} {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
h := BasicAuth("realm", tt.username, tt.password, okHandler())
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
h.ServeHTTP(rec, req)
|
|
||||||
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Errorf("status = %d, want %d (passthrough expected)", rec.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
if rec.Body.String() != "ok" {
|
|
||||||
t.Errorf("body = %q, want %q", rec.Body.String(), "ok")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,38 +1,24 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
|
ErrInvalidMetricsConfig = errors.New("config: invalid metrics configuration")
|
||||||
ErrInvalidMetricsTLSConfig = errors.New("config: invalid metrics TLS configuration")
|
|
||||||
ErrInvalidMetricsBasicAuthConfig = errors.New("config: invalid metrics basic auth configuration")
|
|
||||||
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
|
ErrNoMetricsBind = errors.New("config.Metrics: must define bind")
|
||||||
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
|
ErrNoMetricsNetwork = errors.New("config.Metrics: must define network")
|
||||||
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
|
ErrNoMetricsSocketMode = errors.New("config.Metrics: must define socket mode when using unix sockets")
|
||||||
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
|
ErrInvalidMetricsSocketMode = errors.New("config.Metrics: invalid unix socket mode")
|
||||||
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
|
ErrInvalidMetricsNetwork = errors.New("config.Metrics: invalid metrics network")
|
||||||
ErrNoMetricsTLSCertificate = errors.New("config.Metrics.TLS: must define certificate file")
|
|
||||||
ErrNoMetricsTLSKey = errors.New("config.Metrics.TLS: must define key file")
|
|
||||||
ErrInvalidMetricsTLSKeypair = errors.New("config.Metrics.TLS: keypair is invalid")
|
|
||||||
ErrInvalidMetricsCACertificate = errors.New("config.Metrics.TLS: invalid CA certificate")
|
|
||||||
ErrCantReadFile = errors.New("config: can't read required file")
|
|
||||||
ErrNoMetricsBasicAuthUsername = errors.New("config.Metrics.BasicAuth: must define username")
|
|
||||||
ErrNoMetricsBasicAuthPassword = errors.New("config.Metrics.BasicAuth: must define password")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metrics struct {
|
type Metrics struct {
|
||||||
Bind string `json:"bind" yaml:"bind"`
|
Bind string `json:"bind" yaml:"bind"`
|
||||||
Network string `json:"network" yaml:"network"`
|
Network string `json:"network" yaml:"network"`
|
||||||
SocketMode string `json:"socketMode" yaml:"socketMode"`
|
SocketMode string `json:"socketMode" yaml:"socketMode"`
|
||||||
TLS *MetricsTLS `json:"tls" yaml:"tls"`
|
|
||||||
BasicAuth *MetricsBasicAuth `json:"basicAuth" yaml:"basicAuth"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Metrics) Valid() error {
|
func (m *Metrics) Valid() error {
|
||||||
@@ -60,107 +46,9 @@ func (m *Metrics) Valid() error {
|
|||||||
errs = append(errs, ErrInvalidMetricsNetwork)
|
errs = append(errs, ErrInvalidMetricsNetwork)
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.TLS != nil {
|
|
||||||
if err := m.TLS.Valid(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.BasicAuth != nil {
|
|
||||||
if err := m.BasicAuth.Valid(); err != nil {
|
|
||||||
errs = append(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
if len(errs) != 0 {
|
||||||
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
|
return errors.Join(ErrInvalidMetricsConfig, errors.Join(errs...))
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type MetricsTLS struct {
|
|
||||||
Certificate string `json:"certificate" yaml:"certificate"`
|
|
||||||
Key string `json:"key" yaml:"key"`
|
|
||||||
CA string `json:"ca" yaml:"ca"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mt *MetricsTLS) Valid() error {
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
if mt.Certificate == "" {
|
|
||||||
errs = append(errs, ErrNoMetricsTLSCertificate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := canReadFile(mt.Certificate); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Certificate, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if mt.Key == "" {
|
|
||||||
errs = append(errs, ErrNoMetricsTLSKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := canReadFile(mt.Key); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.Key, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := tls.LoadX509KeyPair(mt.Certificate, mt.Key); err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%w: %w", ErrInvalidMetricsTLSKeypair, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if mt.CA != "" {
|
|
||||||
caCert, err := os.ReadFile(mt.CA)
|
|
||||||
if err != nil {
|
|
||||||
errs = append(errs, fmt.Errorf("%w %s: %w", ErrCantReadFile, mt.CA, err))
|
|
||||||
}
|
|
||||||
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(caCert) {
|
|
||||||
errs = append(errs, fmt.Errorf("%w %s", ErrInvalidMetricsCACertificate, mt.CA))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return errors.Join(ErrInvalidMetricsTLSConfig, errors.Join(errs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func canReadFile(fname string) error {
|
|
||||||
fin, err := os.Open(fname)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer fin.Close()
|
|
||||||
|
|
||||||
data := make([]byte, 64)
|
|
||||||
if _, err := fin.Read(data); err != nil {
|
|
||||||
return fmt.Errorf("can't read %s: %w", fname, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type MetricsBasicAuth struct {
|
|
||||||
Username string `json:"username" yaml:"username"`
|
|
||||||
Password string `json:"password" yaml:"password"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (mba *MetricsBasicAuth) Valid() error {
|
|
||||||
var errs []error
|
|
||||||
|
|
||||||
if mba.Username == "" {
|
|
||||||
errs = append(errs, ErrNoMetricsBasicAuthUsername)
|
|
||||||
}
|
|
||||||
|
|
||||||
if mba.Password == "" {
|
|
||||||
errs = append(errs, ErrNoMetricsBasicAuthPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(errs) != 0 {
|
|
||||||
return errors.Join(ErrInvalidMetricsBasicAuthConfig, errors.Join(errs...))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -75,161 +75,6 @@ func TestMetricsValid(t *testing.T) {
|
|||||||
},
|
},
|
||||||
err: ErrInvalidMetricsNetwork,
|
err: ErrInvalidMetricsNetwork,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "invalid TLS config",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{},
|
|
||||||
},
|
|
||||||
err: ErrInvalidMetricsTLSConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "selfsigned TLS cert",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls/selfsigned.crt",
|
|
||||||
Key: "./testdata/tls/selfsigned.key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "wrong path to selfsigned TLS cert",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls2/selfsigned.crt",
|
|
||||||
Key: "./testdata/tls2/selfsigned.key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrCantReadFile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "unparseable TLS cert",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls/invalid.crt",
|
|
||||||
Key: "./testdata/tls/invalid.key",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrInvalidMetricsTLSKeypair,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mTLS with CA",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls/selfsigned.crt",
|
|
||||||
Key: "./testdata/tls/selfsigned.key",
|
|
||||||
CA: "./testdata/tls/minica.pem",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mTLS with nonexistent CA",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls/selfsigned.crt",
|
|
||||||
Key: "./testdata/tls/selfsigned.key",
|
|
||||||
CA: "./testdata/tls/nonexistent.crt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrCantReadFile,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mTLS with invalid CA",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
TLS: &MetricsTLS{
|
|
||||||
Certificate: "./testdata/tls/selfsigned.crt",
|
|
||||||
Key: "./testdata/tls/selfsigned.key",
|
|
||||||
CA: "./testdata/tls/invalid.crt",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
err: ErrInvalidMetricsCACertificate,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "basic auth credentials set",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
BasicAuth: &MetricsBasicAuth{
|
|
||||||
Username: "admin",
|
|
||||||
Password: "hunter2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid basic auth config",
|
|
||||||
input: &Metrics{
|
|
||||||
Bind: ":9090",
|
|
||||||
Network: "tcp",
|
|
||||||
BasicAuth: &MetricsBasicAuth{},
|
|
||||||
},
|
|
||||||
err: ErrInvalidMetricsBasicAuthConfig,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
|
||||||
t.Logf("wanted error: %v", tt.err)
|
|
||||||
t.Logf("got error: %v", err)
|
|
||||||
t.Error("validation failed")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMetricsBasicAuthValid(t *testing.T) {
|
|
||||||
for _, tt := range []struct {
|
|
||||||
name string
|
|
||||||
input *MetricsBasicAuth
|
|
||||||
err error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "both set",
|
|
||||||
input: &MetricsBasicAuth{
|
|
||||||
Username: "admin",
|
|
||||||
Password: "hunter2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty username and password",
|
|
||||||
input: &MetricsBasicAuth{},
|
|
||||||
err: ErrInvalidMetricsBasicAuthConfig,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing username",
|
|
||||||
input: &MetricsBasicAuth{
|
|
||||||
Password: "hunter2",
|
|
||||||
},
|
|
||||||
err: ErrNoMetricsBasicAuthUsername,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing password",
|
|
||||||
input: &MetricsBasicAuth{
|
|
||||||
Username: "admin",
|
|
||||||
},
|
|
||||||
err: ErrNoMetricsBasicAuthPassword,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing both surfaces wrapper error",
|
|
||||||
input: &MetricsBasicAuth{},
|
|
||||||
err: ErrNoMetricsBasicAuthUsername,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing both surfaces password error",
|
|
||||||
input: &MetricsBasicAuth{},
|
|
||||||
err: ErrNoMetricsBasicAuthPassword,
|
|
||||||
},
|
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
if err := tt.input.Valid(); !errors.Is(err, tt.err) {
|
||||||
|
|||||||
-12
@@ -1,12 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIB1zCCAVygAwIBAgIIYO0SAFtXlVgwCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
|
||||||
bWluaWNhIHJvb3QgY2EgNDE2MmMwMB4XDTI2MDQyMjIzMjUwMVoXDTI4MDUyMjIz
|
|
||||||
MjUwMVowEjEQMA4GA1UEAxMHMS4xLjEuMTB2MBAGByqGSM49AgEGBSuBBAAiA2IA
|
|
||||||
BLsuA2LKGbEBuSA4LTm1KaKc7/QCkUOsipXR4+D5/3sWBZiAH7iWUgHwpx5YZf2q
|
|
||||||
kZn6oRda+ks0JLTQ6VhteQedmb7l86bMeDMR8p4Lg2b38l/xEr7S25UfUDKudXrO
|
|
||||||
AqNxMG8wDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEF
|
|
||||||
BQcDAjAMBgNVHRMBAf8EAjAAMB8GA1UdIwQYMBaAFE/7VDxF2+cUs9bu0pJM3xoC
|
|
||||||
L1TSMA8GA1UdEQQIMAaHBAEBAQEwCgYIKoZIzj0EAwMDaQAwZgIxAPLXds9MMH4K
|
|
||||||
F5FxTf9i0PKPsLQARsABVTgwB94hMR70rqW8Pwbjl7ZGNaYlaeRHUwIxAPMQ8zoF
|
|
||||||
nim+YS1xLqQek/LXuJto8jxcfkQQBsboVzcTa5uaNRhNd5YwrpomGl3lKA==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDBN8QsHxxHGJpStu8K7
|
|
||||||
D/FmaBBNo6c514KGFSIfqGFuREF5aOL3gN/W11yk2OIibdWhZANiAAS7LgNiyhmx
|
|
||||||
AbkgOC05tSminO/0ApFDrIqV0ePg+f97FgWYgB+4llIB8KceWGX9qpGZ+qEXWvpL
|
|
||||||
NCS00OlYbXkHnZm+5fOmzHgzEfKeC4Nm9/Jf8RK+0tuVH1AyrnV6zgI=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDr9QQo7ZaTgUL6d73G
|
|
||||||
2BG7+YRTFJHAZa0FogRglfc+jYttL1J4/xTig3RmHoqSgrehZANiAASDhijM9Xe0
|
|
||||||
G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UX
|
|
||||||
NnhBrukLaXvgAQQq782/5IUYGsvK5jw8+dSscYVMCQJwGfmQuaNeczQ=
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
Vendored
-13
@@ -1,13 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIB+zCCAYKgAwIBAgIIQWLAtv4ijQ0wCgYIKoZIzj0EAwMwIDEeMBwGA1UEAxMV
|
|
||||||
bWluaWNhIHJvb3QgY2EgNDE2MmMwMCAXDTI2MDQyMjIzMjUwMVoYDzIxMjYwNDIy
|
|
||||||
MjMyNTAxWjAgMR4wHAYDVQQDExVtaW5pY2Egcm9vdCBjYSA0MTYyYzAwdjAQBgcq
|
|
||||||
hkjOPQIBBgUrgQQAIgNiAASDhijM9Xe0G9Vam6AJMeKC6aWDNSLwrxNVmPxemsY/
|
|
||||||
yJ1urBgnxRd9GFH6YW1ki/B8rS+Xl1UXNnhBrukLaXvgAQQq782/5IUYGsvK5jw8
|
|
||||||
+dSscYVMCQJwGfmQuaNeczSjgYYwgYMwDgYDVR0PAQH/BAQDAgKEMB0GA1UdJQQW
|
|
||||||
MBQGCCsGAQUFBwMBBggrBgEFBQcDAjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
|
|
||||||
DgQWBBRP+1Q8RdvnFLPW7tKSTN8aAi9U0jAfBgNVHSMEGDAWgBRP+1Q8RdvnFLPW
|
|
||||||
7tKSTN8aAi9U0jAKBggqhkjOPQQDAwNnADBkAjBfY7vb7cuLTjg7uoe+kl07FMYT
|
|
||||||
BGMSnWdhN3yXqMUS3A6XZxD/LntXT6V7yFOlAJYCMH7w8/ATYaTqbk2jBRyQt9/x
|
|
||||||
ajN+kZ6ZK+fKttqE8CD62mbHg09xoNxRq+K2I3PVyQ==
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-11
@@ -1,11 +0,0 @@
|
|||||||
-----BEGIN CERTIFICATE-----
|
|
||||||
MIIBnzCCAVGgAwIBAgIUK39B3Ft+kU5o81IuISs79O4u1ncwBQYDK2VwMEUxCzAJ
|
|
||||||
BgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5l
|
|
||||||
dCBXaWRnaXRzIFB0eSBMdGQwHhcNMjYwNDIyMTQyNjE4WhcNMjYwNTIyMTQyNjE4
|
|
||||||
WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwY
|
|
||||||
SW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMCowBQYDK2VwAyEAfgpAUpp8MIOOdQpH
|
|
||||||
fxaw3R7mFKQRMR6Kmxzk1Xn/2VujUzBRMB0GA1UdDgQWBBSmkBmzo0RiZ2iocMR8
|
|
||||||
uIIpz9cZyTAfBgNVHSMEGDAWgBSmkBmzo0RiZ2iocMR8uIIpz9cZyTAPBgNVHRMB
|
|
||||||
Af8EBTADAQH/MAUGAytlcANBAG37XXZrVUUzGyy3T9qsPIzvJQAGpGhdjJ7bt9O6
|
|
||||||
sBhzrliTONPrudYuyUggWsHgFb0JlN2xs4/2HhKU+PY7AAQ=
|
|
||||||
-----END CERTIFICATE-----
|
|
||||||
-3
@@ -1,3 +0,0 @@
|
|||||||
-----BEGIN PRIVATE KEY-----
|
|
||||||
MC4CAQAwBQYDK2VwBCIEIL0HxjjfVlg6zQPB9/zTLq0IBzfp8gEoifEYzQZYIj+T
|
|
||||||
-----END PRIVATE KEY-----
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type KeypairReloader struct {
|
|
||||||
certMu sync.RWMutex
|
|
||||||
cert *tls.Certificate
|
|
||||||
certPath string
|
|
||||||
keyPath string
|
|
||||||
modTime time.Time
|
|
||||||
lg *slog.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewKeypairReloader(certPath, keyPath string, lg *slog.Logger) (*KeypairReloader, error) {
|
|
||||||
result := &KeypairReloader{
|
|
||||||
certPath: certPath,
|
|
||||||
keyPath: keyPath,
|
|
||||||
lg: lg,
|
|
||||||
}
|
|
||||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.cert = &cert
|
|
||||||
|
|
||||||
st, err := os.Stat(certPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.modTime = st.ModTime()
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kpr *KeypairReloader) maybeReload() error {
|
|
||||||
kpr.lg.Debug("loading new keypair", "cert", kpr.certPath, "key", kpr.keyPath)
|
|
||||||
newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
st, err := os.Stat(kpr.certPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
kpr.certMu.Lock()
|
|
||||||
defer kpr.certMu.Unlock()
|
|
||||||
kpr.cert = &newCert
|
|
||||||
kpr.modTime = st.ModTime()
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (kpr *KeypairReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
st, err := os.Stat(kpr.certPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("stat(%q): %w", kpr.certPath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
kpr.certMu.RLock()
|
|
||||||
needsReload := st.ModTime().After(kpr.modTime)
|
|
||||||
kpr.certMu.RUnlock()
|
|
||||||
|
|
||||||
if needsReload {
|
|
||||||
if err := kpr.maybeReload(); err != nil {
|
|
||||||
return nil, fmt.Errorf("reload cert: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kpr.certMu.RLock()
|
|
||||||
defer kpr.certMu.RUnlock()
|
|
||||||
return kpr.cert, nil
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
package metrics
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"log/slog"
|
|
||||||
"math/big"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func discardLogger() *slog.Logger {
|
|
||||||
return slog.New(slog.DiscardHandler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeKeypair generates a fresh self-signed cert + RSA key and writes them
|
|
||||||
// as PEM files in dir. Returns the paths and the cert's DER bytes so callers
|
|
||||||
// can identify which pair was loaded.
|
|
||||||
func writeKeypair(t *testing.T, dir, prefix string) (certPath, keyPath string, certDER []byte) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("rsa.GenerateKey: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpl := &x509.Certificate{
|
|
||||||
SerialNumber: big.NewInt(time.Now().UnixNano()),
|
|
||||||
Subject: pkix.Name{CommonName: "keypairreloader-test"},
|
|
||||||
NotBefore: time.Now().Add(-time.Hour),
|
|
||||||
NotAfter: time.Now().Add(time.Hour),
|
|
||||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
|
||||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
|
||||||
BasicConstraintsValid: true,
|
|
||||||
DNSNames: []string{"keypairreloader-test"},
|
|
||||||
}
|
|
||||||
|
|
||||||
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &priv.PublicKey, priv)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("x509.CreateCertificate: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certPath = filepath.Join(dir, prefix+"cert.pem")
|
|
||||||
keyPath = filepath.Join(dir, prefix+"key.pem")
|
|
||||||
|
|
||||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
|
||||||
|
|
||||||
if err := os.WriteFile(certPath, certPEM, 0o600); err != nil {
|
|
||||||
t.Fatalf("write cert: %v", err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(keyPath, keyPEM, 0o600); err != nil {
|
|
||||||
t.Fatalf("write key: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return certPath, keyPath, der
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewKeypairReloader(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
goodCert, goodKey, _ := writeKeypair(t, dir, "good-")
|
|
||||||
|
|
||||||
garbagePath := filepath.Join(dir, "garbage.pem")
|
|
||||||
if err := os.WriteFile(garbagePath, []byte("not a pem file"), 0o600); err != nil {
|
|
||||||
t.Fatalf("write garbage: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
certPath string
|
|
||||||
keyPath string
|
|
||||||
wantErr error
|
|
||||||
wantNil bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid cert and key",
|
|
||||||
certPath: goodCert,
|
|
||||||
keyPath: goodKey,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing cert file",
|
|
||||||
certPath: filepath.Join(dir, "does-not-exist.pem"),
|
|
||||||
keyPath: goodKey,
|
|
||||||
wantErr: os.ErrNotExist,
|
|
||||||
wantNil: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "missing key file",
|
|
||||||
certPath: goodCert,
|
|
||||||
keyPath: filepath.Join(dir, "does-not-exist-key.pem"),
|
|
||||||
wantErr: os.ErrNotExist,
|
|
||||||
wantNil: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cert file is garbage",
|
|
||||||
certPath: garbagePath,
|
|
||||||
keyPath: goodKey,
|
|
||||||
wantNil: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
kpr, err := NewKeypairReloader(tt.certPath, tt.keyPath, discardLogger())
|
|
||||||
|
|
||||||
if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
|
|
||||||
t.Errorf("err = %v, want errors.Is(..., %v)", err, tt.wantErr)
|
|
||||||
}
|
|
||||||
if tt.wantErr == nil && !tt.wantNil && err != nil {
|
|
||||||
t.Errorf("unexpected err: %v", err)
|
|
||||||
}
|
|
||||||
if tt.wantNil && kpr != nil {
|
|
||||||
t.Errorf("kpr = %+v, want nil", kpr)
|
|
||||||
}
|
|
||||||
if !tt.wantNil && kpr == nil {
|
|
||||||
t.Errorf("kpr is nil, want non-nil")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestKeypairReloader_GetCertificate(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
run func(t *testing.T)
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "returns loaded cert",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
certPath, keyPath, wantDER := writeKeypair(t, dir, "a-")
|
|
||||||
|
|
||||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewKeypairReloader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := kpr.GetCertificate(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetCertificate: %v", err)
|
|
||||||
}
|
|
||||||
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], wantDER) {
|
|
||||||
t.Errorf("GetCertificate returned wrong cert bytes")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "reloads when mtime advances",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
|
||||||
|
|
||||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewKeypairReloader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite with a new pair at the same paths and bump mtime.
|
|
||||||
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
|
||||||
mustRename(t, newCertPath, certPath)
|
|
||||||
mustRename(t, newKeyPath, keyPath)
|
|
||||||
future := time.Now().Add(time.Hour)
|
|
||||||
if err := os.Chtimes(certPath, future, future); err != nil {
|
|
||||||
t.Fatalf("Chtimes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := kpr.GetCertificate(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetCertificate: %v", err)
|
|
||||||
}
|
|
||||||
if len(got.Certificate) == 0 || !bytes.Equal(got.Certificate[0], newDER) {
|
|
||||||
t.Errorf("GetCertificate did not return reloaded cert")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "does not reload when mtime unchanged",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
certPath, keyPath, originalDER := writeKeypair(t, dir, "a-")
|
|
||||||
|
|
||||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewKeypairReloader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite the cert/key files with a *different* keypair, then
|
|
||||||
// rewind mtime so the reloader must not pick up the change.
|
|
||||||
newCertPath, newKeyPath, newDER := writeKeypair(t, dir, "b-")
|
|
||||||
mustRename(t, newCertPath, certPath)
|
|
||||||
mustRename(t, newKeyPath, keyPath)
|
|
||||||
past := time.Unix(0, 0)
|
|
||||||
if err := os.Chtimes(certPath, past, past); err != nil {
|
|
||||||
t.Fatalf("Chtimes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, err := kpr.GetCertificate(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("GetCertificate: %v", err)
|
|
||||||
}
|
|
||||||
if len(got.Certificate) == 0 {
|
|
||||||
t.Fatal("empty cert chain")
|
|
||||||
}
|
|
||||||
if bytes.Equal(got.Certificate[0], newDER) {
|
|
||||||
t.Errorf("GetCertificate reloaded despite unchanged mtime")
|
|
||||||
}
|
|
||||||
if !bytes.Equal(got.Certificate[0], originalDER) {
|
|
||||||
t.Errorf("GetCertificate did not return original cert")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "does not panic when reload fails after mtime bump",
|
|
||||||
run: func(t *testing.T) {
|
|
||||||
dir := t.TempDir()
|
|
||||||
certPath, keyPath, _ := writeKeypair(t, dir, "a-")
|
|
||||||
|
|
||||||
kpr, err := NewKeypairReloader(certPath, keyPath, discardLogger())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("NewKeypairReloader: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Corrupt the cert file and bump mtime. maybeReload will fail.
|
|
||||||
if err := os.WriteFile(certPath, []byte("not a pem file"), 0o600); err != nil {
|
|
||||||
t.Fatalf("corrupt cert: %v", err)
|
|
||||||
}
|
|
||||||
future := time.Now().Add(time.Hour)
|
|
||||||
if err := os.Chtimes(certPath, future, future); err != nil {
|
|
||||||
t.Fatalf("Chtimes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
t.Fatalf("GetCertificate panicked on reload failure: %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
got, err := kpr.GetCertificate(nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Errorf("GetCertificate returned nil err for corrupt cert; got %+v", got)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
tt.run(t)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mustRename(t *testing.T, from, to string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.Rename(from, to); err != nil {
|
|
||||||
t.Fatalf("rename %q -> %q: %v", from, to, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-50
@@ -2,14 +2,11 @@ package metrics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/TecharoHQ/anubis/internal"
|
"github.com/TecharoHQ/anubis/internal"
|
||||||
@@ -23,16 +20,10 @@ type Server struct {
|
|||||||
Log *slog.Logger
|
Log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) Run(ctx context.Context, done func()) {
|
func (s *Server) Run(ctx context.Context, done func()) error {
|
||||||
defer done()
|
defer done()
|
||||||
lg := s.Log.With("subsystem", "metrics")
|
lg := s.Log.With("subsystem", "metrics")
|
||||||
|
|
||||||
if err := s.run(ctx, lg); err != nil {
|
|
||||||
lg.Error("can't serve metrics server", "err", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
mux.HandleFunc("GET /debug/pprof/", pprof.Index)
|
||||||
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
|
mux.HandleFunc("GET /debug/pprof/cmdline", pprof.Cmdline)
|
||||||
@@ -71,39 +62,6 @@ func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
|||||||
|
|
||||||
defer ln.Close()
|
defer ln.Close()
|
||||||
|
|
||||||
if s.Config.TLS != nil {
|
|
||||||
kpr, err := NewKeypairReloader(s.Config.TLS.Certificate, s.Config.TLS.Key, lg)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("can't setup keypair reloader: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.TLSConfig = &tls.Config{
|
|
||||||
GetCertificate: kpr.GetCertificate,
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Config.TLS.CA != "" {
|
|
||||||
caCert, err := os.ReadFile(s.Config.TLS.CA)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w %s: %w", config.ErrCantReadFile, s.Config.TLS.CA, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
certPool := x509.NewCertPool()
|
|
||||||
if !certPool.AppendCertsFromPEM(caCert) {
|
|
||||||
return fmt.Errorf("%w %s", config.ErrInvalidMetricsCACertificate, s.Config.TLS.CA)
|
|
||||||
}
|
|
||||||
|
|
||||||
srv.TLSConfig.ClientCAs = certPool
|
|
||||||
srv.TLSConfig.ClientAuth = tls.RequireAndVerifyClientCert
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Config.BasicAuth != nil {
|
|
||||||
var h http.Handler = mux
|
|
||||||
h = internal.BasicAuth("anubis-metrics", s.Config.BasicAuth.Username, s.Config.BasicAuth.Password, mux)
|
|
||||||
|
|
||||||
srv.Handler = h
|
|
||||||
}
|
|
||||||
|
|
||||||
lg.Debug("listening for metrics", "url", metricsURL)
|
lg.Debug("listening for metrics", "url", metricsURL)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
@@ -115,16 +73,9 @@ func (s *Server) run(ctx context.Context, lg *slog.Logger) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
switch s.Config.TLS != nil {
|
|
||||||
case true:
|
|
||||||
if err := srv.ServeTLS(ln, "", ""); !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
return fmt.Errorf("can't serve TLS metrics server: %w", err)
|
|
||||||
}
|
|
||||||
case false:
|
|
||||||
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
if err := srv.Serve(ln); !errors.Is(err, http.ErrServerClosed) {
|
||||||
return fmt.Errorf("can't serve metrics server: %w", err)
|
return fmt.Errorf("can't serve metrics server: %w", err)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ User-agent: meta-externalagent
|
|||||||
User-agent: Meta-ExternalAgent
|
User-agent: Meta-ExternalAgent
|
||||||
User-agent: meta-externalfetcher
|
User-agent: meta-externalfetcher
|
||||||
User-agent: Meta-ExternalFetcher
|
User-agent: Meta-ExternalFetcher
|
||||||
User-agent: meta-webindexer
|
|
||||||
User-agent: MistralAI-User
|
User-agent: MistralAI-User
|
||||||
User-agent: MistralAI-User/1.0
|
User-agent: MistralAI-User/1.0
|
||||||
User-agent: MyCentralAIScraperBot
|
User-agent: MyCentralAIScraperBot
|
||||||
|
|||||||
Reference in New Issue
Block a user