mirror of
https://github.com/ryan4yin/nix-config.git
synced 2026-11-13 12:13:46 +01:00
The homelab hosts accept root SSH with long-lived file keys, run several exposed services with almost no systemd sandboxing, and keep no audit trail of privileged actions. Close those three gaps, weighing each measure against its cost instead of adopting a policy wholesale. SSH - Turn off KbdInteractiveAuthentication: nixpkgs defaults it to true and PAM then still accepts the Unix password despite PasswordAuthentication = false. - authorizedKeysInHomedir = false: only keys declared in Nix take effect; youko keeps Gitea's forced-command file through `Match User gitea`. - Pin aquamarine's passphrase-free remote-builder keys to `restrict,command="nix-daemon --stdio --store daemon"`, so builds run in the system daemon instead of an audited root login session. systemd sandboxing - Add the mylib.systemdHardening preset and apply it to the exposed, weakly sandboxed services: caddy 7.8 -> 1.5, vmalert 8.2 -> 1.4, rustfs 6.6 -> 1.4, whisper-asr 7.7 -> 1.6. An eval test pins the key restrictions. - Rejected: a global all-keys policy (<=0.2 score gains on services nixpkgs already sandboxes) and per-service confinement (bind paths to maintain). auditd on all four hosts, reusing nixpkgs' loader and unit - Audit login sessions only: a first `-a never,task -F auid=unset` rule keeps services out of the per-syscall audit path. A compromised service is therefore unrecorded until it opens a session. - Record root commands, credential and host-key access, writes to SSH, account, PAM, Nix-trust and audit configuration, module loads and session denials. - Rejected: global denial logging, service-UID watches and --loginuid-immutable (the kernel already records loginuid changes). Verified on all four hosts after deployment: zero failed units, k3s 6/6 Ready, and the exposed services healthy. Assisted-by: Claude Code (claude-opus-5-5)
179 lines
7.2 KiB
Nix
179 lines
7.2 KiB
Nix
{ config, mylib, ... }:
|
|
{
|
|
# https://docs.victoriametrics.com/victoriametrics/vmalert/
|
|
services.vmalert.instances."homelab" = {
|
|
enable = true;
|
|
settings = {
|
|
"httpListenAddr" = "127.0.0.1:8880";
|
|
|
|
"datasource.url" = "http://localhost:9090";
|
|
"notifier.url" = [ "http://localhost:9093" ]; # alertmanager's api
|
|
# Recording rules results are persisted via remote write.
|
|
"remoteWrite.url" = "http://localhost:9090";
|
|
"remoteRead.url" = "http://localhost:9090";
|
|
|
|
# Whether to disable long-lived connections to the datasource.
|
|
"datasource.disableKeepAlive" = true;
|
|
# Whether to avoid stripping sensitive information such as auth headers or passwords
|
|
# from URLs in log messages or UI and exported metrics.
|
|
"datasource.showURL" = false;
|
|
# Path to the files with alerting and/or recording rules.
|
|
rule = [
|
|
"${./alert_rules}/*.yml"
|
|
"${./alert_rules}/*.yaml"
|
|
"${./recording_rules}/*.yml"
|
|
"${./recording_rules}/*.yaml"
|
|
];
|
|
# https://docs.victoriametrics.com/victoriametrics/vmalert/#link-to-alert-source
|
|
# Set this two args to generate the correct `.GeneratorURL`
|
|
"external.url" = "https://grafana.writefor.fun";
|
|
# Grafana resolves this by UID, then by name; it must match the provisioned
|
|
# datasource name in grafana/datasources.nix.
|
|
"external.alert.source" =
|
|
''explore?left={"datasource":"victoriametrics-homelab","queries":[{"expr":{{ .Expr|jsonEscape|queryEscape }},"refId":"A"}],"range":{"from":"{{ .ActiveAt.UnixMilli }}","to":"now"}}'';
|
|
};
|
|
};
|
|
|
|
# nixpkgs only sets DynamicUser; vmalert talks to loopback and keeps no state.
|
|
systemd.services.vmalert-homelab.serviceConfig = mylib.systemdHardening.mkDefaults mylib.systemdHardening.service;
|
|
|
|
services.prometheus.alertmanager = {
|
|
enable = true;
|
|
listenAddress = "127.0.0.1";
|
|
port = 9093;
|
|
webExternalUrl = "http://alertmanager.writefor.fun";
|
|
logLevel = "info";
|
|
environmentFile = config.age.secrets."alertmanager.env".path;
|
|
configuration = {
|
|
global = {
|
|
# The smarthost and SMTP sender used for mail notifications.
|
|
smtp_smarthost = "smtp.qq.com:465";
|
|
smtp_from = "$SMTP_SENDER_EMAIL";
|
|
smtp_auth_username = "$SMTP_AUTH_USERNAME";
|
|
smtp_auth_password = "$SMTP_AUTH_PASSWORD";
|
|
# smtp.qq.com:465 support SSL only, so we need to disable TLS here.
|
|
# https://service.mail.qq.com/detail/0/310
|
|
smtp_require_tls = false;
|
|
};
|
|
route = {
|
|
receiver = "telegram";
|
|
routes = [
|
|
{
|
|
# Meta alerts (Watchdog, InfoInhibitor) and info-level noise never notify.
|
|
receiver = "null";
|
|
matchers = [ ''severity =~ "none|info"'' ];
|
|
}
|
|
{
|
|
receiver = "telegram";
|
|
# group alerts by labels
|
|
group_by = [
|
|
"job"
|
|
# --- Alert labels ---
|
|
"alertname"
|
|
"alertgroup"
|
|
# --- kubernetes labels ---
|
|
"namespace"
|
|
# --- custom labels ---
|
|
"cluster"
|
|
"env"
|
|
"type"
|
|
];
|
|
group_wait = "3m"; # wait for other alerts to "group by" before send notification
|
|
group_interval = "5m"; # wait for an interval, before send a new alert in the same group
|
|
repeat_interval = "5h"; # avoiding repeating reminders too frequently
|
|
}
|
|
# {
|
|
# # Route only prod env's critical alerts to email (most severe alerts)
|
|
# match = {
|
|
# severity = "critical";
|
|
# env = "prd";
|
|
# };
|
|
# receiver = "email";
|
|
# group_by = [
|
|
# "host"
|
|
# "namespace"
|
|
# "pod"
|
|
# "job"
|
|
# ];
|
|
# group_wait = "1m";
|
|
# group_interval = "5m";
|
|
# repeat_interval = "2h";
|
|
# }
|
|
];
|
|
};
|
|
receivers = [
|
|
{
|
|
# Discards all notifications (for meta alerts that should never notify).
|
|
name = "null";
|
|
}
|
|
# {
|
|
# name = "email";
|
|
# email_configs = [
|
|
# {
|
|
# to = "ryan4yin@linux.com";
|
|
# # Whether to notify about resolved alerts.
|
|
# send_resolved = true;
|
|
# }
|
|
# ];
|
|
# }
|
|
{
|
|
name = "telegram";
|
|
telegram_configs = [
|
|
{
|
|
bot_token = "$TELEGRAM_BOT_TOKEN";
|
|
chat_id = 586169186; # My Telegram ID
|
|
# Whether to notify about resolved alerts.
|
|
send_resolved = true;
|
|
# Not disabled: resolved alerts are still notified (see send_resolved).
|
|
disable_notifications = false;
|
|
# Telegram's MarkdownV2 & Markdown are all very painful, we use html instead.
|
|
# https://core.telegram.org/bots/api#formatting-options
|
|
parse_mode = "HTML";
|
|
# Message template
|
|
# Telegram limits a message to 4096 chars, so only render the first 5 alerts
|
|
# in a group and keep the fields minimal (no full label dump).
|
|
#
|
|
# WARNING: never use template variables ($i, $a, ...) here. The NixOS
|
|
# alertmanager module pipes the generated config through `envsubst`
|
|
# (to inject $TELEGRAM_BOT_TOKEN etc.), which silently replaces any
|
|
# $var in this template with an empty string and breaks rendering at
|
|
# notify time. Use `{{ range .Alerts }}` + dot, and `define`/`template`
|
|
# for reuse instead.
|
|
message = ''
|
|
{{- if eq .Status "firing" }}
|
|
🟡 <b>告警触发</b> [{{ .CommonLabels.severity | title }}] <b>{{ .CommonLabels.alertname }}</b> (共 {{ len .Alerts }} 条)
|
|
{{- else }}
|
|
🟢 <b>告警恢复</b> [{{ .CommonLabels.severity | title }}] <b>{{ .CommonLabels.alertname }}</b>
|
|
{{- end }}
|
|
|
|
• <b>告警组</b>: {{ with .CommonLabels.alertgroup }}{{ . }}{{ else }}N/A{{ end }}
|
|
• <b>Cluster</b>: {{ with .CommonLabels.cluster }}{{ . }}{{ else }}N/A{{ end }}
|
|
• <b>Env</b>: {{ with .CommonLabels.env }}{{ . }}{{ else }}N/A{{ end }}
|
|
• <b>Namespace</b>: {{ with .CommonLabels.namespace }}{{ . }}{{ else }}N/A{{ end }}
|
|
{{- define "alert" }}
|
|
|
|
━━━━━━━━
|
|
📌 {{ with .Annotations.summary }}<b>{{ . }}</b>{{ else }}<b>{{ .Labels.instance }}</b>{{ end }}
|
|
{{- with .Labels.nodename }}
|
|
🏠 {{ . }}
|
|
{{- end }}
|
|
🔗 <a href="{{ .GeneratorURL }}">Grafana Explore</a> · ⏱ {{ .StartsAt.Format "2006-01-02 15:04:05" }}
|
|
{{- end }}
|
|
{{- if le (len .Alerts) 5 }}
|
|
{{- range .Alerts }}{{ template "alert" . }}
|
|
{{- end }}
|
|
{{- else }}
|
|
{{- range slice .Alerts 0 5 }}{{ template "alert" . }}
|
|
{{- end }}
|
|
|
|
⚠️ 另有 {{ len (slice .Alerts 5) }} 条同组告警未列出,请查看 Alertmanager。
|
|
{{- end }}
|
|
'';
|
|
}
|
|
];
|
|
}
|
|
];
|
|
};
|
|
};
|
|
}
|