From 67f43298e82768269ee5e4e97b89cc31fa868d2a Mon Sep 17 00:00:00 2001 From: chayleaf Date: Tue, 24 Oct 2023 00:19:12 +0700 Subject: [PATCH] server: add certspotter --- flake.nix | 1 + pkgs/certspotter/configurable-sendmail.patch | 71 ++++++++++++ pkgs/certspotter/default.nix | 41 +++++++ pkgs/default.nix | 1 + system/hosts/server/default.nix | 19 ++++ system/modules/certspotter.nix | 112 +++++++++++++++++++ 6 files changed, 245 insertions(+) create mode 100644 pkgs/certspotter/configurable-sendmail.patch create mode 100644 pkgs/certspotter/default.nix create mode 100644 system/modules/certspotter.nix diff --git a/flake.nix b/flake.nix index 7dff09d..cfcdc4d 100644 --- a/flake.nix +++ b/flake.nix @@ -156,6 +156,7 @@ ./system/devices/radxa-rock5a-server.nix (if devMaubot then import /${devPath}/maubot.nix/module else maubot.nixosModules.default) ./system/modules/scanservjs.nix + ./system/modules/certspotter.nix ]; }; server-cross = crossConfig server; diff --git a/pkgs/certspotter/configurable-sendmail.patch b/pkgs/certspotter/configurable-sendmail.patch new file mode 100644 index 0000000..c895a76 --- /dev/null +++ b/pkgs/certspotter/configurable-sendmail.patch @@ -0,0 +1,71 @@ +diff --git a/cmd/certspotter/main.go b/cmd/certspotter/main.go +index 9730789..f2eb081 100644 +--- a/cmd/certspotter/main.go ++++ b/cmd/certspotter/main.go +@@ -163,6 +163,7 @@ func main() { + logs string + noSave bool + script string ++ sendmail string + startAtEnd bool + stateDir string + stdout bool +@@ -176,6 +177,7 @@ func main() { + flag.StringVar(&flags.logs, "logs", defaultLogList, "File path or URL of JSON list of logs to monitor") + flag.BoolVar(&flags.noSave, "no_save", false, "Do not save a copy of matching certificates in state directory") + flag.StringVar(&flags.script, "script", "", "Program to execute when a matching certificate is discovered") ++ flag.StringVar(&flags.sendmail, "sendmail", "/usr/sbin/sendmail", "Path to the sendmail-compatible program to use") + flag.BoolVar(&flags.startAtEnd, "start_at_end", false, "Start monitoring logs from the end rather than the beginning (saves considerable bandwidth)") + flag.StringVar(&flags.stateDir, "state_dir", defaultStateDir(), "Directory for storing log position and discovered certificates") + flag.BoolVar(&flags.stdout, "stdout", false, "Write matching certificates to stdout") +@@ -201,6 +203,7 @@ func main() { + Verbose: flags.verbose, + Script: flags.script, + ScriptDir: defaultScriptDir(), ++ SendmailPath: flags.sendmail, + Email: flags.email, + Stdout: flags.stdout, + HealthCheckInterval: flags.healthcheck, +diff --git a/monitor/config.go b/monitor/config.go +index 1e0d60c..d1bc430 100644 +--- a/monitor/config.go ++++ b/monitor/config.go +@@ -20,6 +20,7 @@ type Config struct { + WatchList WatchList + Verbose bool + SaveCerts bool ++ SendmailPath string + Script string + ScriptDir string + Email []string +diff --git a/monitor/notify.go b/monitor/notify.go +index 8fc6d09..86cabca 100644 +--- a/monitor/notify.go ++++ b/monitor/notify.go +@@ -36,7 +36,7 @@ func notify(ctx context.Context, config *Config, notif notification) error { + } + + if len(config.Email) > 0 { +- if err := sendEmail(ctx, config.Email, notif); err != nil { ++ if err := sendEmail(ctx, config.SendmailPath, config.Email, notif); err != nil { + return err + } + } +@@ -62,7 +62,7 @@ func writeToStdout(notif notification) { + os.Stdout.WriteString(notif.Text() + "\n") + } + +-func sendEmail(ctx context.Context, to []string, notif notification) error { ++func sendEmail(ctx context.Context, sendmailPath string, to []string, notif notification) error { + stdin := new(bytes.Buffer) + stderr := new(bytes.Buffer) + +@@ -77,7 +77,7 @@ func sendEmail(ctx context.Context, to []string, notif notification) error { + args := []string{"-i", "--"} + args = append(args, to...) + +- sendmail := exec.CommandContext(ctx, "/usr/sbin/sendmail", args...) ++ sendmail := exec.CommandContext(ctx, sendmailPath, args...) + sendmail.Stdin = stdin + sendmail.Stderr = stderr + diff --git a/pkgs/certspotter/default.nix b/pkgs/certspotter/default.nix new file mode 100644 index 0000000..30904c8 --- /dev/null +++ b/pkgs/certspotter/default.nix @@ -0,0 +1,41 @@ +{ lib +, buildGoModule +, fetchFromGitHub +, lowdown +}: + +buildGoModule rec { + pname = "certspotter"; + version = "0.16.0"; + + src = fetchFromGitHub { + owner = "SSLMate"; + repo = "certspotter"; + rev = "v${version}"; + hash = "sha256-0+7GWxbV4j2vVdmool8J9hqRqUi8O/yKedCyynWJDkE="; + }; + + vendorHash = "sha256-haYmWc2FWZNFwMhmSy3DAtj9oW5G82dX0fxpGqI8Hbw="; + + patches = [ ./configurable-sendmail.patch ]; + + ldflags = [ "-s" "-w" ]; + + nativeBuildInputs = [ lowdown ]; + + postInstall = '' + cd man + make + mkdir -p $out/share/man/man8 + mv *.8 $out/share/man/man8 + ''; + + meta = with lib; { + description = "Certificate Transparency Log Monitor"; + homepage = "https://github.com/SSLMate/certspotter"; + changelog = "https://github.com/SSLMate/certspotter/blob/${src.rev}/CHANGELOG.md"; + license = licenses.mpl20; + mainProgram = "certspotter"; + maintainers = with maintainers; [ chayleaf ]; + }; +} diff --git a/pkgs/default.nix b/pkgs/default.nix index 60d5372..61038a1 100644 --- a/pkgs/default.nix +++ b/pkgs/default.nix @@ -60,6 +60,7 @@ in meta = builtins.removeAttrs old.meta [ "broken" ]; }); + certspotter = callPackage ./certspotter { }; clang-tools_latest = pkgs.clang-tools_16; clang_latest = pkgs.clang_16; /*ghidra = pkgs.ghidra.overrideAttrs (old: { diff --git a/system/hosts/server/default.nix b/system/hosts/server/default.nix index 95c7b0f..e249bcf 100644 --- a/system/hosts/server/default.nix +++ b/system/hosts/server/default.nix @@ -349,6 +349,25 @@ in { }; }; + users.users.certspotter.extraGroups = [ "acme" ]; + services.certspotter = { + enable = true; + watchlist = [ ".pavluk.org" ]; + hooks = let + openssl = "${pkgs.openssl.bin}/bin/openssl"; + in lib.toList (pkgs.writeShellScript "certspotter-hook" '' + if [[ "$EVENT" == discovered_cert ]]; then + mkdir -p /var/lib/certspotter/allowed_tbs + for cert in $(find /var/lib/acme -regex ".*/fullchain.pem"); do + hash="$(${openssl} x509 -in "$cert" -pubkey -noout | ${openssl} pkey -pubin -outform DER | ${openssl} sha256 | cut -d" " -f2)" + touch "/var/lib/certspotter/allowed_tbs/$hash" + done + [[ -f "/var/lib/certspotter/allowed_tbs/$TBS_SHA256" ]] && exit 0 + fi + (echo "Subject: $SUMMARY" && echo && cat "$TEXT_FILENAME") | /run/wrappers/bin/sendmail -i webmaster-certspotter@${cfg.domainName} + ''); + }; + /*locations."/dns-query".extraConfig = '' grpc_pass grpc://127.0.0.1:53453; '';*/ diff --git a/system/modules/certspotter.nix b/system/modules/certspotter.nix new file mode 100644 index 0000000..64b02a5 --- /dev/null +++ b/system/modules/certspotter.nix @@ -0,0 +1,112 @@ +{ config +, lib +, pkgs +, ... }: + +let + cfg = config.services.certspotter; +in { + options.services.certspotter = { + enable = lib.mkEnableOption "Cert Spotter, a Certificate Transparency log monitor"; + sendmailPath = lib.mkOption { + type = lib.types.path; + description = '' + Path to the `sendmail` binary. By default, the local sendmail wrapper is used + (see `config.services.mail.sendmailSetuidWrapper`). + ''; + example = lib.literalExpression ''"''${pkgs.system-sendmail}/bin/sendmail"''; + }; + watchlist = lib.mkOption { + type = with lib.types; listOf str; + description = "Domain names to watch. To monitor a domain with all subdomains, prefix its name with `.` (e.g. `.example.org`)."; + default = [ ]; + example = [ ".example.org" "another.example.com" ]; + }; + emailRecipients = lib.mkOption { + type = with lib.types; listOf str; + description = "A list of email addresses to send certificate updates to."; + default = [ ]; + }; + hooks = lib.mkOption { + type = with lib.types; listOf path; + description = '' + Scripts to run upon the detection of a new certificate. See `man 8 certspotter-script` or [the GitHub page](https://github.com/SSLMate/certspotter/blob/master/man/certspotter-script.md) for more info. + ''; + default = []; + example = lib.literalExpression '' + [ + (pkgs.writeShellScript "certspotter-hook" ''' + echo "Event summary: $SUMMARY." + ''') + ] + ''; + }; + extraFlags = lib.mkOption { + type = with lib.types; listOf str; + description = "Extra command-line arguments to pass to Cert Spotter"; + example = [ "-start_at_end" ]; + default = [ ]; + }; + }; + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.watchlist != [ ]; + message = "You must specify at least one domain for Cert Spotter to watch"; + } + { + assertion = cfg.hooks != [] || cfg.emailRecipients != []; + message = "You must specify at least one hook or email recipient for Cert Spotter"; + } + { + assertion = (cfg.emailRecipients != []) -> (cfg.sendmailPath != "/run/current-system/sw/bin/false"); + message = '' + You must configure the sendmail setuid wrapper (services.mail.sendmailSetuidWrapper) + or services.certspotter.sendmailPath + ''; + } + ]; + services.certspotter.sendmailPath = lib.mkMerge [ + (lib.mkIf (config.services.mail.sendmailSetuidWrapper != null) (lib.mkOptionDefault "/run/wrappers/bin/sendmail")) + (lib.mkIf (config.services.mail.sendmailSetuidWrapper == null) (lib.mkOptionDefault "/run/current-system/sw/bin/false")) + ]; + users.users.certspotter = { + group = "certspotter"; + home = "/var/lib/certspotter"; + createHome = true; + isSystemUser = true; + # uid = config.ids.uids.certspotter; + }; + users.groups.certspotter = { + # gid = config.ids.gids.certspotter; + }; + systemd.services.certspotter = { + description = "Cert Spotter - Certificate Transparency Monitor"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + environment.CERTSPOTTER_CONFIG_DIR = pkgs.linkFarm "certspotter-config" + (lib.toList { + name = "watchlist"; + path = pkgs.writeText "cerspotter-watchlist" (builtins.concatStringsSep "\n" cfg.watchlist); + } + ++ lib.optional (cfg.emailRecipients != [ ]) { + name = "email_recipients"; + path = pkgs.writeText "cerspotter-email_recipients" (builtins.concatStringsSep "\n" cfg.emailRecipients); + } + ++ lib.optional (cfg.hooks != [ ]) { + name = "hooks.d"; + path = pkgs.linkFarm "certspotter-hooks" (lib.imap1 (i: path: { + inherit path; + name = "hook${toString i}"; + }) cfg.hooks); + }); + environment.CERTSPOTTER_STATE_DIR = "/var/lib/certspotter"; + serviceConfig = { + User = "certspotter"; + Group = "certspotter"; + WorkingDirectory = "/var/lib/certspotter"; + ExecStart = "${pkgs.certspotter}/bin/certspotter -sendmail ${cfg.sendmailPath} ${lib.escapeShellArgs cfg.extraFlags}"; + }; + }; + }; +}