From a9ff168fd6fb765025f8ee47c88d5f280851daca Mon Sep 17 00:00:00 2001 From: naturallaw77 Date: Sat, 15 Aug 2026 23:00:59 -0500 Subject: [PATCH] security: prevent LND admin macaroon exposure in curl argv --- modules/bitcoin/lnd.nix | 5 ++++- tests/test_security.py | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/modules/bitcoin/lnd.nix b/modules/bitcoin/lnd.nix index 9a8cdef..70a6e2a 100644 --- a/modules/bitcoin/lnd.nix +++ b/modules/bitcoin/lnd.nix @@ -264,13 +264,16 @@ in { curl = "${pkgs.curl}/bin/curl -fsS --cacert ${cfg.certPath}"; restUrl = "https://${nbLib.addressWithPort cfg.restAddress cfg.restPort}/v1"; # Setting macaroon permissions for other users needs root permissions + # The admin macaroon is passed to curl via a fd because argv is + # world-readable through /proc//cmdline script = nbLib.rootScript "lnd-create-macaroons" '' umask ug=r,o= ${lib.concatMapStrings (macaroon: '' echo "Create custom macaroon ${macaroon}" macaroonPath="$RUNTIME_DIRECTORY/${macaroon}.macaroon" + adminMacaroonHex=$(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon') ${curl} \ - -H "Grpc-Metadata-macaroon: $(${pkgs.xxd}/bin/xxd -ps -u -c 99999 '${networkDir}/admin.macaroon')" \ + -H @<(printf 'Grpc-Metadata-macaroon: %s\n' "$adminMacaroonHex") \ -X POST \ -d '{"permissions":[${cfg.macaroons.${macaroon}.permissions}]}' \ ${restUrl}/macaroon |\ diff --git a/tests/test_security.py b/tests/test_security.py index 08e214c..7c07744 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -333,6 +333,33 @@ class TestSshPubkeyValidation(unittest.TestCase): _validate_ssh_pubkey("ssh-ed25519") +# --------------------------------------------------------------------------- +# LND macaroon command-line safety +# --------------------------------------------------------------------------- + +class TestLndMacaroonCommandLineSafety(unittest.TestCase): + """The LND admin macaroon must never be exposed in curl's argv.""" + + @classmethod + def setUpClass(cls): + path = os.path.join(_REPO_ROOT, "modules", "bitcoin", "lnd.nix") + with open(path, encoding="utf-8") as f: + cls.lnd_module = f.read() + + def test_admin_macaroon_not_interpolated_into_header_argument(self): + self.assertNotIn( + '-H "Grpc-Metadata-macaroon: $(', + self.lnd_module, + ) + + def test_admin_macaroon_header_is_passed_via_file_descriptor(self): + self.assertIn("adminMacaroonHex=$(", self.lnd_module) + self.assertIn( + """-H @<(printf 'Grpc-Metadata-macaroon: %s\\n' "$adminMacaroonHex")""", + self.lnd_module, + ) + + # --------------------------------------------------------------------------- # Auth-exempt paths # ---------------------------------------------------------------------------