mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
189 lines
6.8 KiB
Go
189 lines
6.8 KiB
Go
package instancecheck
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/warmbly/warmbly/internal/app/instanceconfig"
|
|
"github.com/warmbly/warmbly/internal/config"
|
|
)
|
|
|
|
const (
|
|
docsSecrets = "/development/configuration/#secrets"
|
|
docsEncryption = "/development/configuration/#encryption"
|
|
docsWorkers = "/development/configuration/#workers"
|
|
docsProxy = "/development/configuration/#network-and-proxy"
|
|
docsCaptcha = "/development/configuration/#captcha"
|
|
docsDeployment = "/development/configuration/#deployment"
|
|
docsWebhooks = "/guides/webhooks/"
|
|
docsMail = "/development/configuration/#platform-mail"
|
|
)
|
|
|
|
func securityChecks() []check {
|
|
return []check{
|
|
{id: "secret_published_default", run: checkSecretPublishedDefault},
|
|
{id: "allow_insecure_defaults", run: checkAllowInsecureDefaults},
|
|
{id: "credentials_key_unset", run: checkCredentialsKeyUnset},
|
|
{id: "internal_token_unset", run: checkInternalTokenUnset},
|
|
{id: "trusted_proxies_unset", run: checkTrustedProxiesUnset},
|
|
{id: "captcha_misconfigured", run: checkCaptchaMisconfigured},
|
|
{id: "turnstile_bypass_set", run: checkTurnstileBypassSet},
|
|
{id: "dev_mode_public", run: checkDevModePublic},
|
|
{id: "unsafe_webhook_urls", run: checkUnsafeWebhookURLs},
|
|
{id: "tls_verification_off", run: checkTLSVerificationOff},
|
|
}
|
|
}
|
|
|
|
func checkSecretPublishedDefault(ctx context.Context, d Deps, in Input) *Finding {
|
|
keys := make([]string, 0, len(instanceconfig.PublishedDefaults))
|
|
for key := range instanceconfig.PublishedDefaults {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
var offenders []string
|
|
for _, key := range keys {
|
|
if os.Getenv(key) == instanceconfig.PublishedDefaults[key] {
|
|
offenders = append(offenders, key)
|
|
}
|
|
}
|
|
if len(offenders) == 0 {
|
|
return nil
|
|
}
|
|
|
|
severity := SeverityError
|
|
if devEnv() || truthy("ALLOW_INSECURE_DEFAULTS") {
|
|
severity = SeverityWarning
|
|
}
|
|
|
|
verb := "hold"
|
|
if len(offenders) == 1 {
|
|
verb = "holds"
|
|
}
|
|
|
|
return result(CategorySecurity, severity, "Published default secrets in use",
|
|
fmt.Sprintf("%s still %s the published default value from docker-compose.yml. "+
|
|
"Those values are public in the Warmbly repository, so anyone can forge a session token "+
|
|
"or unwrap every organization key. Generate real values with `make gen-key` and restart.",
|
|
strings.Join(offenders, ", "), verb),
|
|
docsSecrets)
|
|
}
|
|
|
|
func checkAllowInsecureDefaults(ctx context.Context, d Deps, in Input) *Finding {
|
|
if !truthy("ALLOW_INSECURE_DEFAULTS") {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityWarning, "Insecure defaults are allowed",
|
|
"ALLOW_INSECURE_DEFAULTS=true is set, so this instance starts even when it is using published default secrets. "+
|
|
"Remove it once you have generated real values.",
|
|
docsSecrets)
|
|
}
|
|
|
|
func checkCredentialsKeyUnset(ctx context.Context, d Deps, in Input) *Finding {
|
|
if env("CREDENTIALS_ENCRYPTION_KEY") != "" {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityError, "Mailbox credentials are not sealed",
|
|
"CREDENTIALS_ENCRYPTION_KEY is not set, so mailbox SMTP and IMAP passwords are stored without being sealed. "+
|
|
"Set a 64 hex character key (`openssl rand -hex 32`) before connecting any mailbox. "+
|
|
"Back it up: losing it makes connected mailboxes unrecoverable.",
|
|
docsEncryption)
|
|
}
|
|
|
|
func checkInternalTokenUnset(ctx context.Context, d Deps, in Input) *Finding {
|
|
if env("INTERNAL_API_TOKEN") != "" {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityError, "Internal API token is not set",
|
|
"INTERNAL_API_TOKEN is not set, so every request to /api/v1/internal/ is rejected. "+
|
|
"Workers cannot fetch organization keys and the tracking service cannot resolve click links. "+
|
|
"Nothing fails at boot, only at runtime.",
|
|
docsWorkers)
|
|
}
|
|
|
|
func checkTrustedProxiesUnset(ctx context.Context, d Deps, in Input) *Finding {
|
|
// Only a real forwarded request proves a proxy is in front; guessing would
|
|
// warn every directly exposed backend, where empty is correct.
|
|
if !in.Forwarded || env("TRUSTED_PROXIES") != "" {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityWarning, "Proxy headers are not trusted",
|
|
"This request arrived with an X-Forwarded-For header but TRUSTED_PROXIES is empty, "+
|
|
"so Warmbly is recording your proxy's address as the client address. "+
|
|
"The per-IP login limiter, session records, audit rows and API key IP allowlists are all reading the wrong address. "+
|
|
"Set TRUSTED_PROXIES to your proxy's CIDR.",
|
|
docsProxy)
|
|
}
|
|
|
|
func checkCaptchaMisconfigured(ctx context.Context, d Deps, in Input) *Finding {
|
|
if config.CaptchaProvider() != "turnstile" || env("TURNSTILE_SECRET") != "" {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityError, "Captcha cannot verify",
|
|
"CAPTCHA_PROVIDER is turnstile but TURNSTILE_SECRET is empty, so every captcha verification fails "+
|
|
"and nobody can sign in. Set the secret or set CAPTCHA_PROVIDER=none.",
|
|
docsCaptcha)
|
|
}
|
|
|
|
func checkTurnstileBypassSet(ctx context.Context, d Deps, in Input) *Finding {
|
|
if env("TURNSTILE_BYPASS_TOKEN") == "" {
|
|
return nil
|
|
}
|
|
severity := SeverityWarning
|
|
if devEnv() {
|
|
severity = SeverityInfo
|
|
}
|
|
return result(CategorySecurity, severity, "Turnstile bypass token is set",
|
|
"TURNSTILE_BYPASS_TOKEN is set. It is only honoured when APP_ENV=dev, so on this deployment it does nothing. "+
|
|
"Remove it to avoid confusion.",
|
|
docsCaptcha)
|
|
}
|
|
|
|
func checkDevModePublic(ctx context.Context, d Deps, in Input) *Finding {
|
|
if !devEnv() || isLoopbackURL(appURL()) {
|
|
return nil
|
|
}
|
|
value := env("APP_ENV")
|
|
if value == "" {
|
|
value = "unset"
|
|
}
|
|
return result(CategorySecurity, SeverityWarning, "Dev mode on a public address",
|
|
fmt.Sprintf("This instance is running with APP_ENV=%s on a public address. "+
|
|
"Dev mode allows the published default secrets and enables Gin debug logging. Set APP_ENV=prod.", value),
|
|
docsDeployment)
|
|
}
|
|
|
|
func checkUnsafeWebhookURLs(ctx context.Context, d Deps, in Input) *Finding {
|
|
if !truthy("WARMBLY_ALLOW_UNSAFE_WEBHOOK_URLS") {
|
|
return nil
|
|
}
|
|
return result(CategorySecurity, SeverityWarning, "Unsafe webhook URLs are allowed",
|
|
"Customer webhooks may point at http:// and private addresses. This is intended for development only, "+
|
|
"because it lets any workspace member make the backend reach into your internal network.",
|
|
docsWebhooks)
|
|
}
|
|
|
|
func checkTLSVerificationOff(ctx context.Context, d Deps, in Input) *Finding {
|
|
platform := truthy("SMTP_TLS_INSECURE_SKIP_VERIFY")
|
|
mailbox := truthy("MAIL_TLS_INSECURE")
|
|
if !platform && !mailbox {
|
|
return nil
|
|
}
|
|
|
|
subject := "mailbox connections"
|
|
switch {
|
|
case platform && mailbox:
|
|
subject = "platform mail and mailbox connections"
|
|
case platform:
|
|
subject = "platform mail"
|
|
}
|
|
|
|
return result(CategorySecurity, SeverityWarning, "TLS verification is disabled",
|
|
fmt.Sprintf("TLS certificate verification is disabled for %s. "+
|
|
"Only do this for a relay using a private certificate authority.", subject),
|
|
docsMail)
|
|
}
|