diff --git a/nixos/tests/litestream.nix b/nixos/tests/litestream.nix index f5a1668c30f2..f879226e6f56 100644 --- a/nixos/tests/litestream.nix +++ b/nixos/tests/litestream.nix @@ -1,4 +1,8 @@ -{ pkgs, ... }: +{ pkgs, lib, ... }: +let + dbPath = "/var/lib/grafana/data/grafana.db"; + socketPath = "/run/litestream/litestream.sock"; +in { name = "litestream"; meta = with pkgs.lib.maintainers; { @@ -11,38 +15,53 @@ services.litestream = { enable = true; settings = { + socket = { + enabled = true; + path = socketPath; + }; dbs = [ { - path = "/var/lib/grafana/data/grafana.db"; + path = dbPath; replicas = [ { url = "sftp://foo:bar@127.0.0.1:22/home/foo/grafana"; + auto-recover = true; } ]; } ]; }; }; - systemd.services.grafana.serviceConfig.ExecStartPost = - "+" - + pkgs.writeShellScript "grant-grafana-permissions" '' - timeout=10 - - while [ ! -f /var/lib/grafana/data/grafana.db ]; - do - if [ "$timeout" == 0 ]; then - echo "ERROR: Timeout while waiting for /var/lib/grafana/data/grafana.db." - exit 1 - fi - - sleep 1 - - ((timeout--)) - done - - find /var/lib/grafana -type d -exec chmod -v 775 {} \; - find /var/lib/grafana -type f -exec chmod -v 660 {} \; - ''; + # Litestream 0.5.x writes to the database (_litestream_seq table), + # so grafana's data directory must be group-writable. + systemd.tmpfiles.settings."10-litestream" = { + "/var/lib/grafana".d = { + mode = "0750"; + user = "grafana"; + group = "grafana"; + }; + "/var/lib/grafana/data".d = { + mode = "2770"; + user = "grafana"; + group = "grafana"; + }; + }; + systemd.services.grafana.serviceConfig = { + ExecStartPre = lib.mkAfter "+${pkgs.sqlite}/bin/sqlite3 ${dbPath} 'PRAGMA journal_mode=WAL;'"; + UMask = lib.mkForce "0007"; + }; + systemd.services.litestream = { + after = [ + "grafana.service" + "sshd.service" + ]; + requires = [ "grafana.service" ]; + wants = [ "sshd.service" ]; + serviceConfig = { + RuntimeDirectory = "litestream"; + ExecStartPre = "+/bin/sh -c 'chmod g+rw ${dbPath}*'"; + }; + }; services.openssh = { enable = true; allowSFTP = true; @@ -59,6 +78,7 @@ security = { admin_user = "admin"; admin_password = "admin"; + secret_key = "SW2YcwTIb9zpOOhoPsMm"; }; server = { @@ -68,7 +88,7 @@ database = { type = "sqlite3"; - path = "/var/lib/grafana/data/grafana.db"; + path = dbPath; wal = true; }; }; @@ -78,35 +98,44 @@ password = "bar"; }; users.users.litestream.extraGroups = [ "grafana" ]; + environment.systemPackages = [ pkgs.sqlite ]; }; testScript = '' start_all() - machine.wait_until_succeeds("test -d /home/foo/grafana") machine.wait_for_open_port(3000) - machine.succeed(""" - curl -sSfN -X PUT -H "Content-Type: application/json" -d '{ - "oldPassword": "admin", - "newPassword": "newpass", - "confirmNew": "newpass" - }' http://admin:admin@127.0.0.1:3000/api/user/password - """) - # https://litestream.io/guides/systemd/#simulating-a-disaster - machine.systemctl("stop litestream.service") - machine.succeed( - "rm -f /var/lib/grafana/data/grafana.db " - "/var/lib/grafana/data/grafana.db-shm " - "/var/lib/grafana/data/grafana.db-wal" - ) - machine.succeed( - "litestream restore /var/lib/grafana/data/grafana.db " - "&& chown grafana:grafana /var/lib/grafana/data/grafana.db " - "&& chmod 660 /var/lib/grafana/data/grafana.db" - ) - machine.systemctl("restart grafana.service") - machine.wait_for_open_port(3000) - machine.succeed( - "curl -sSfN -u admin:newpass http://127.0.0.1:3000/api/org/users | grep admin\@localhost" - ) + with subtest("Verify litestream replicates changes"): + machine.succeed(""" + curl -sSfN -X PUT --json '{ + "name": "LitestreamTest", + "login": "admin", + "email": "admin@localhost" + }' http://admin:admin@127.0.0.1:3000/api/user + """) + machine.succeed("litestream sync -wait -socket ${socketPath} ${dbPath}") + machine.succeed( + "litestream restore -o /tmp/restored.db ${dbPath} && " + "sqlite3 /tmp/restored.db '.dump' | grep -q LitestreamTest" + ) + + with subtest("Simulate disaster recovery"): + # https://litestream.io/guides/systemd/#simulating-a-disaster + machine.systemctl("stop litestream.service grafana.service") + machine.succeed( + "rm -f ${dbPath} " + "${dbPath}-shm " + "${dbPath}-wal" + ) + machine.succeed( + "litestream restore ${dbPath} " + "&& chown grafana:grafana ${dbPath} " + "&& chmod 660 ${dbPath}" + ) + machine.systemctl("restart grafana.service litestream.service") + machine.wait_for_open_port(3000) + machine.wait_for_unit("litestream.service") + machine.succeed( + "curl -sSfN -u admin:admin http://127.0.0.1:3000/api/user | grep LitestreamTest" + ) ''; } diff --git a/pkgs/by-name/li/litestream/fix-cve-2024-41254.patch b/pkgs/by-name/li/litestream/fix-cve-2024-41254.patch deleted file mode 100644 index 8fd06b712a24..000000000000 --- a/pkgs/by-name/li/litestream/fix-cve-2024-41254.patch +++ /dev/null @@ -1,64 +0,0 @@ -diff --git a/cmd/litestream/main.go b/cmd/litestream/main.go -index 1234567..abcdefg 100644 ---- a/cmd/litestream/main.go -+++ b/cmd/litestream/main.go -@@ -362,6 +362,7 @@ type ReplicaConfig struct { - Host string `yaml:"host"` - User string `yaml:"user"` - Password string `yaml:"password"` - KeyPath string `yaml:"key-path"` -+ HostKeyPath string `yaml:"host-key-path"` - - // Encryption identities and recipients -@@ -664,6 +665,7 @@ func NewReplicaFromConfig(c *ReplicaConfig, dbc *DBConfig) (_ litestream.Replic - client.Password = password - client.Path = path - client.KeyPath = c.KeyPath -+ client.HostKeyPath = c.HostKeyPath - return client, nil - } - -diff --git a/sftp/replica_client.go b/sftp/replica_client.go -index 30d8fa87..8b651e97 100644 ---- a/sftp/replica_client.go -+++ b/sftp/replica_client.go -@@ -41,6 +41,7 @@ type ReplicaClient struct { - Password string - Path string - KeyPath string -+ HostKeyPath string - DialTimeout time.Duration - } - -@@ -71,14 +72,28 @@ func (c *ReplicaClient) Init(ctx context.Context) (_ *sftp.Client, err error) { - - // Build SSH configuration & auth methods - config := &ssh.ClientConfig{ -- User: c.User, -- HostKeyCallback: ssh.InsecureIgnoreHostKey(), -- BannerCallback: ssh.BannerDisplayStderr(), -+ User: c.User, -+ BannerCallback: ssh.BannerDisplayStderr(), - } - if c.Password != "" { - config.Auth = append(config.Auth, ssh.Password(c.Password)) - } - -+ if c.HostKeyPath == "" { -+ config.HostKeyCallback = ssh.InsecureIgnoreHostKey() -+ } else { -+ buf, err := os.ReadFile(c.HostKeyPath) -+ if err != nil { -+ return nil, fmt.Errorf("cannot read sftp host key path: %w", err) -+ } -+ -+ key, _, _, _, err := ssh.ParseAuthorizedKey(buf) -+ if err != nil { -+ return nil, fmt.Errorf("cannot parse sftp host key path: path=%s len=%d err=%w", c.HostKeyPath, len(buf), err) -+ } -+ config.HostKeyCallback = ssh.FixedHostKey(key) -+ } -+ - if c.KeyPath != "" { - buf, err := os.ReadFile(c.KeyPath) - if err != nil { \ No newline at end of file diff --git a/pkgs/by-name/li/litestream/package.nix b/pkgs/by-name/li/litestream/package.nix index f5d68da741a8..fad210501e68 100644 --- a/pkgs/by-name/li/litestream/package.nix +++ b/pkgs/by-name/li/litestream/package.nix @@ -6,13 +6,13 @@ }: buildGoModule (finalAttrs: { pname = "litestream"; - version = "0.3.14"; + version = "0.5.10"; src = fetchFromGitHub { owner = "benbjohnson"; repo = "litestream"; rev = "v${finalAttrs.version}"; - sha256 = "sha256-rTPw1BnZPtcWzhwR9mCzoE4LXdlaFfRCIDMHlN/SRzk="; + hash = "sha256-xp1Ic7sF3yzpR4FgMOfx/uRp/jv/qzTgSlItOIrl2pI="; }; ldflags = [ @@ -21,9 +21,7 @@ buildGoModule (finalAttrs: { "-X main.Version=${finalAttrs.version}" ]; - vendorHash = "sha256-sYIY3Z3VrCqbjEbQtEY7q6Jljg8jMoa2qWEB/IkDjzM="; - - patches = [ ./fix-cve-2024-41254.patch ]; + vendorHash = "sha256-e2fsgK/fZNIos5W/Gc3u72uzoT2igs6BgzYtz1PyI10="; passthru.tests = { inherit (nixosTests) litestream; };