Unverified Commit c4e4953f authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(auth): 公开登录门按来源限速并设全站发信上限,冷却表定期清理

parent 15f729ff
Loading
Loading
Loading
Loading
+22 −0
Changes for cmd/felis/api.go: 22 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -300,8 +300,20 @@ func cmdAPI(args []string, stdout, stderr io.Writer) int {
		// for legitimate multi-tab / multi-server watching, while capping how many
		// upstream follow connections a single caller can tie up if their streams stall.
		MaxStreamsPerPrincipal: 16,
		// Public sign-in doors, per client address: a person signing in makes a
		// handful of calls, so 20 at once refilled at 20 a minute never bites a
		// real user and still turns a spray into a trickle. The client address
		// is the edge's header when the install names one (config.AuthConfig).
		AuthDoorLimit:  api.RateLimit{Burst: 20, PerMinute: 20},
		ClientIPHeader: cfg.Auth.EffectiveClientIPHeader(),
		MailLimit:      mailLimit(cfg.SMTP.MaxPerHour),
	}
	fmt.Fprintln(stderr, "felis api: external face fails closed (Access JWKS key function not configured)")
	if a.ClientIPHeader != "" {
		fmt.Fprintf(stderr, "felis api: sign-in rate limit keys on the %s header\n", a.ClientIPHeader)
	} else {
		fmt.Fprintln(stderr, "felis api: sign-in rate limit keys on the TCP peer ([auth] client_ip_header unset)")
	}

	// Felis-nano: the multi-source hasJoined multiplexer. Mojang leads as the code-owned
	// identity anchor (正版优先); config can only append namespace-rewritten third-party
@@ -546,3 +558,13 @@ func reconcileBuilds(ctx context.Context, b *build.Builder, stderr io.Writer) {
		}
	}
}

// mailLimit turns smtp.max_per_hour into the API's install-wide mail bucket:
// the hourly cap as the refill rate, with a quarter of it (at least 5) allowed
// at once so a burst of real sign-ins is not queued behind the average.
func mailLimit(perHour int) api.RateLimit {
	if perHour <= 0 {
		perHour = config.DefaultMailPerHour
	}
	return api.RateLimit{Burst: max(perHour/4, 5), PerMinute: float64(perHour) / 60}
}
+10 −5
Changes for cmd/felis/tui_edge_apply.go: 10 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -32,7 +32,9 @@ func applyCloudflareEdge(ctx context.Context, result *cfsetup.Result, panelHost,
	if adminHost == "" {
		return fmt.Errorf("admin hostname is required")
	}
	if err := writeConnectionConfig(panelHost, adminHost, result.AccessAud); err != nil {
	// cloudflared is the only way in once the NodePort is fenced, so the
	// visitor address it writes can key the sign-in rate limit.
	if err := writeConnectionConfig(panelHost, adminHost, result.AccessAud, "CF-Connecting-IP"); err != nil {
		return err
	}
	if err := applyFelisConfigSecret(ctx); err != nil {
@@ -78,7 +80,8 @@ func applyReverseProxy(ctx context.Context, panelHost, adminHost string) error {
	if adminHost == "" {
		return fmt.Errorf("admin hostname is required")
	}
	if err := writeConnectionConfig(panelHost, adminHost, ""); err != nil {
	// Caddy, nginx and Traefik all append the peer they saw to X-Forwarded-For.
	if err := writeConnectionConfig(panelHost, adminHost, "", "X-Forwarded-For"); err != nil {
		return err
	}
	if err := applyFelisConfigSecret(ctx); err != nil {
@@ -93,16 +96,17 @@ func applyReverseProxy(ctx context.Context, panelHost, adminHost string) error {
// writeConnectionConfig stamps the chosen hostnames (and optional Access audience)
// into both the host and pod config files. An empty aud clears any prior
// Cloudflare audience, which is correct when switching to a non-Access front.
func writeConnectionConfig(panelHost, adminHost, aud string) error {
// clientIPHeader is the header that front writes the visitor address into.
func writeConnectionConfig(panelHost, adminHost, aud, clientIPHeader string) error {
	for _, path := range []string{hostSetupConfigPath, podSetupConfigPath} {
		if err := updateAuthConfig(path, panelHost, adminHost, aud); err != nil {
		if err := updateAuthConfig(path, panelHost, adminHost, aud, clientIPHeader); err != nil {
			return err
		}
	}
	return nil
}

func updateAuthConfig(path, panelHost, adminHost, aud string) error {
func updateAuthConfig(path, panelHost, adminHost, aud, clientIPHeader string) error {
	cfg, err := config.Load(path)
	if err != nil {
		return err
@@ -112,6 +116,7 @@ func updateAuthConfig(path, panelHost, adminHost, aud string) error {
	}
	cfg.Auth.AdminHostname = adminHost
	cfg.Auth.AccessJWTAud = aud
	cfg.Auth.ClientIPHeader = clientIPHeader
	return writeConfig(path, cfg)
}

+49 −1
Changes for deploy/alerts/felis-alerts.yaml: 49 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -4,7 +4,9 @@
#
# felis_* series come from two processes:
#   - felis-operator pod :8080/metrics      → felis_servers_total, felis_start_duration_seconds
#   - felis-api internal :8081/metrics      → felis_image_build_failures_total
#   - felis-api internal :8081/metrics      → felis_image_build_failures_total,
#                                             felis_mail_total, felis_rate_limited_total,
#                                             felis_auth_otp_lockouts_total
#   - node-exporter textfile collector      → felis_db_backup_* (felis-db-backup.timer)
# node_* / kube_* series come from node-exporter / kube-state-metrics.
groups:
@@ -94,3 +96,49 @@ groups:
            --collector.textfile.directory at the directory of
            FELIS_DB_BACKUP_METRICS (default /var/lib/node_exporter/textfile_collector)
            (troubleshooting §16).
  - name: felis.auth.rules
    rules:
      - alert: FelisMailBudgetExhausted
        expr: sum(increase(felis_mail_total{result="throttled"}[15m])) > 0
        labels:
          severity: warning
        annotations:
          summary: "the install-wide mail budget refused mail"
          description: >-
            felis_mail_total{result="throttled"} increased: [smtp] max_per_hour is
            spent, and every sign-in code is refused with 429 mail_rate_limited until
            it refills. Check felis_rate_limited_total for a flood before raising the
            budget (troubleshooting §17).
      - alert: FelisMailDeliveryFailing
        expr: sum(increase(felis_mail_total{result="failed"}[15m])) > 0
        labels:
          severity: warning
        annotations:
          summary: "the SMTP relay refused mail in the last 15m"
          description: >-
            felis_mail_total{result="failed"} increased: sign-in codes are not being
            delivered (502 mail_undeliverable). The relay's reason is in the
            felis-api log (troubleshooting §17).
      - alert: FelisSignInFlood
        expr: sum(rate(felis_rate_limited_total{scope="auth_door"}[5m])) * 60 > 10
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "sign-in doors refusing over 10 requests a minute"
          description: >-
            The per-address sign-in limit has been refusing callers for 10 minutes.
            A script is hammering the auth doors; if real users report rate_limited
            at once instead, [auth] client_ip_header is missing and everyone shares
            the proxy's address (troubleshooting §17).
      - alert: FelisOTPAccountLocked
        expr: sum by (purpose) (increase(felis_auth_otp_lockouts_total[1h])) > 0
        labels:
          severity: warning
        annotations:
          summary: "an account's email-code sign-in locked after 10 wrong codes"
          description: >-
            Someone entered 10 wrong codes for one account within 24h ({{ $labels.purpose }}).
            The audit log names the account (action auth.otp.locked); the owner was
            mailed. Unless they fumbled codes, someone is guessing at it
            (troubleshooting §17).
+85 −0
Changes for deploy/alerts/felis-alerts_test.yml: 85 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -151,3 +151,88 @@ tests:
                --collector.textfile.directory at the directory of
                FELIS_DB_BACKUP_METRICS (default /var/lib/node_exporter/textfile_collector)
                (troubleshooting §16).
  - name: sign-in mail budget and relay
    interval: 1m
    input_series:
      # Created at zero on start; the budget refuses one mail at t=3m.
      - series: 'felis_mail_total{kind="otp",result="throttled",job="felis-api"}'
        values: '0 0 0 1x30'
      - series: 'felis_mail_total{kind="otp",result="failed",job="felis-api"}'
        values: '0x33'
    alert_rule_test:
      - eval_time: 2m
        alertname: FelisMailBudgetExhausted
        exp_alerts: []
      - eval_time: 5m
        alertname: FelisMailBudgetExhausted
        exp_alerts:
          - exp_labels:
              severity: warning
            exp_annotations:
              summary: "the install-wide mail budget refused mail"
              description: >-
                felis_mail_total{result="throttled"} increased: [smtp] max_per_hour is
                spent, and every sign-in code is refused with 429 mail_rate_limited until
                it refills. Check felis_rate_limited_total for a flood before raising the
                budget (troubleshooting §17).
      - eval_time: 5m
        alertname: FelisMailDeliveryFailing
        exp_alerts: []
  - name: sign-in flood
    interval: 1m
    input_series:
      # 30 refusals a minute from t=0; a lone refused script at 2/min stays quiet.
      - series: 'felis_rate_limited_total{scope="auth_door",job="felis-api"}'
        values: '0+30x40'
    alert_rule_test:
      - eval_time: 10m
        alertname: FelisSignInFlood
        exp_alerts: []
      - eval_time: 20m
        alertname: FelisSignInFlood
        exp_alerts:
          - exp_labels:
              severity: warning
            exp_annotations:
              summary: "sign-in doors refusing over 10 requests a minute"
              description: >-
                The per-address sign-in limit has been refusing callers for 10 minutes.
                A script is hammering the auth doors; if real users report rate_limited
                at once instead, [auth] client_ip_header is missing and everyone shares
                the proxy's address (troubleshooting §17).
  - name: sign-in trickle stays quiet
    interval: 1m
    input_series:
      - series: 'felis_rate_limited_total{scope="auth_door",job="felis-api"}'
        values: '0+2x40'
    alert_rule_test:
      - eval_time: 30m
        alertname: FelisSignInFlood
        exp_alerts: []
  - name: account email-code lock
    interval: 1m
    input_series:
      - series: 'felis_auth_otp_lockouts_total{purpose="login_email",job="felis-api"}'
        values: '0 0 1x90'
      - series: 'felis_auth_otp_lockouts_total{purpose="op_login",job="felis-api"}'
        values: '0x92'
    alert_rule_test:
      - eval_time: 1m
        alertname: FelisOTPAccountLocked
        exp_alerts: []
      - eval_time: 10m
        alertname: FelisOTPAccountLocked
        exp_alerts:
          - exp_labels:
              severity: warning
              purpose: login_email
            exp_annotations:
              summary: "an account's email-code sign-in locked after 10 wrong codes"
              description: >-
                Someone entered 10 wrong codes for one account within 24h (login_email).
                The audit log names the account (action auth.otp.locked); the owner was
                mailed. Unless they fumbled codes, someone is guessing at it
                (troubleshooting §17).
      - eval_time: 90m
        alertname: FelisOTPAccountLocked
        exp_alerts: []
+46 −0
Changes for deploy/alerts/felis-prometheusrule.yaml: 46 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -98,3 +98,49 @@ spec:
              --collector.textfile.directory at the directory of
              FELIS_DB_BACKUP_METRICS (default /var/lib/node_exporter/textfile_collector)
              (troubleshooting §16).
    - name: felis.auth.rules
      rules:
        - alert: FelisMailBudgetExhausted
          expr: sum(increase(felis_mail_total{result="throttled"}[15m])) > 0
          labels:
            severity: warning
          annotations:
            summary: "the install-wide mail budget refused mail"
            description: >-
              felis_mail_total{result="throttled"} increased: [smtp] max_per_hour is
              spent, and every sign-in code is refused with 429 mail_rate_limited until
              it refills. Check felis_rate_limited_total for a flood before raising the
              budget (troubleshooting §17).
        - alert: FelisMailDeliveryFailing
          expr: sum(increase(felis_mail_total{result="failed"}[15m])) > 0
          labels:
            severity: warning
          annotations:
            summary: "the SMTP relay refused mail in the last 15m"
            description: >-
              felis_mail_total{result="failed"} increased: sign-in codes are not being
              delivered (502 mail_undeliverable). The relay's reason is in the
              felis-api log (troubleshooting §17).
        - alert: FelisSignInFlood
          expr: sum(rate(felis_rate_limited_total{scope="auth_door"}[5m])) * 60 > 10
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "sign-in doors refusing over 10 requests a minute"
            description: >-
              The per-address sign-in limit has been refusing callers for 10 minutes.
              A script is hammering the auth doors; if real users report rate_limited
              at once instead, [auth] client_ip_header is missing and everyone shares
              the proxy's address (troubleshooting §17).
        - alert: FelisOTPAccountLocked
          expr: sum by (purpose) (increase(felis_auth_otp_lockouts_total[1h])) > 0
          labels:
            severity: warning
          annotations:
            summary: "an account's email-code sign-in locked after 10 wrong codes"
            description: >-
              Someone entered 10 wrong codes for one account within 24h ({{ $labels.purpose }}).
              The audit log names the account (action auth.otp.locked); the owner was
              mailed. Unless they fumbled codes, someone is guessing at it
              (troubleshooting §17).
Loading