diff --git a/README.md b/README.md index 8ed6ee1..fd82eb6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Flat, explicit NixOS modules with locked inputs. Required setup belongs here—n | `neovim.nix`, `neovim-test.lua` | Unmodified upstream editor deployment and opt-in native runtime audit | | `updates.nix`, `update-system.sh`, `update-test.py` | Shared dev-owned checkouts, daily boot-staged updates and failure/concurrency regression tests | | `switch-system.sh`, `switch-test.py` | Same installed manual apply/preview command on both hosts, with host identity supplied by Nix | -| `physical-test.nix`, `tools-test.nix` | Physical/AWS safety, shared-policy assertions and bounded offline tool/help smoke tests | +| `physical-test.nix`, `tools-test.nix`, `git-credentials-test.nix` | Physical/AWS safety, shared-policy assertions, offline tool/help smoke tests and disposable Git credential-cache checks | | `desktop-test.nix`, `desktop-test.py`, `audit-desktop.sh` | Disposable graphical/PAM/audio/scaling audit | | `workstation.nix`, `nvidia.nix` | Shared local hardware/SDDM integration for laptop and VM; separate opt-in NVIDIA support | | [DESKTOP.md](DESKTOP.md) | Live audit, wallpaper provenance, JaKooLit comparison and explicit feature-completion plan | @@ -91,6 +91,28 @@ See [DESKTOP.md](DESKTOP.md) for the screenshot-led audit, functional coverage, These are system-owned executables from Nix, not unmanaged `npm -g`, `pip install --user` or `cargo install` bootstraps. Project dependencies may still be downloaded by their ordinary package managers. `nix develop` / `.envrc` remain appropriate for project-specific versions; this is not a promise that every language project uses the same global toolchain. +### Git credentials: terminal, not a GUI + +Git HTTPS authentication uses terminal username/token prompts and Git's native +**in-memory cache with a 365-day timeout** (`31536000` seconds). Inherited helpers +are reset, and Git/SSH graphical askpass fallback is disabled for normal terminal +Git invocations. No credential-manager GUI or plaintext `credential-store` is used. +Credentials are scoped to the repository path as well as the host. + +This is cache retention, **not a new token expiry**: rebooting, stopping the cache +daemon or rejecting a credential clears it, and the provider can expire/revoke a +token sooner. A successful re-approval refreshes its cache timeout. Choose a +one-year token expiry at your Git provider if it supports it; no real credentials +or provider settings are changed by this configuration. SSH keys/agents and +KeePassXC's storage for other applications remain unchanged. IDEs or repositories +that explicitly override Git helpers/askpass can override these user defaults. + +To forget all cached Git HTTPS credentials immediately: + +```sh +git credential-cache exit +``` + ### Per-project DeepSeek Harness [AGENT-VM.md](AGENT-VM.md) documents the project template and `nix run .#agent`. diff --git a/flake.nix b/flake.nix index 3253fb8..946f5d6 100644 --- a/flake.nix +++ b/flake.nix @@ -63,6 +63,10 @@ in { desktop = import ./desktop-test.nix { inherit inputs pkgs; }; + git-credentials = import ./git-credentials-test.nix { + inherit pkgs; + config = inputs.self.nixosConfigurations.nixos.config; + }; tools = import ./tools-test.nix { inherit pkgs; config = inputs.self.nixosConfigurations.nixos.config; diff --git a/git-credentials-test.nix b/git-credentials-test.nix new file mode 100644 index 0000000..864eca6 --- /dev/null +++ b/git-credentials-test.nix @@ -0,0 +1,80 @@ +# Offline protocol checks using dummy credentials in a disposable HOME only. +{ config, pkgs }: +let + hm = config.home-manager.users.dev; +in +assert + hm.programs.git.settings.credential.helper == [ + "" + "cache --timeout=31536000" + ]; +assert hm.programs.git.settings.credential.useHttpPath; +assert hm.programs.git.settings.core.askPass == ""; +assert hm.home.sessionVariables.GIT_ASKPASS == ""; +assert hm.home.sessionVariables.GIT_TERMINAL_PROMPT == "1"; +pkgs.runCommand "git-terminal-credentials-check" + { + nativeBuildInputs = [ + config.programs.git.package + pkgs.coreutils + pkgs.gnugrep + pkgs.expect + ]; + } + '' + export HOME="$TMPDIR/home" XDG_CONFIG_HOME="$TMPDIR/home/.config" XDG_CACHE_HOME="$TMPDIR/cache" + export GIT_CONFIG_NOSYSTEM=1 GIT_ASKPASS="" GIT_TERMINAL_PROMPT=0 + mkdir -p "$XDG_CONFIG_HOME/git" + cp ${hm.xdg.configFile."git/config".source} "$XDG_CONFIG_HOME/git/config" + test "$(git config --get core.askPass)" = "" + git config --get-all credential.helper | grep -qx 'cache --timeout=31536000' + trap 'git credential-cache exit' EXIT + + printf 'protocol=https\nhost=git.example.invalid\npath=project.git\nusername=test\npassword=offline-test-token\n\n' | + git credential approve + printf 'protocol=https\nhost=git.example.invalid\npath=project.git\n\n' | + git credential fill > "$TMPDIR/retrieved" + grep -qx 'password=offline-test-token' "$TMPDIR/retrieved" + test -S "$XDG_CACHE_HOME/git/credential/socket" + test ! -e "$HOME/.git-credentials" + + # Even with a GUI fallback in the environment, a cache miss must not invoke it. + printf '#!${pkgs.runtimeShell}\ntouch "$TMPDIR/gui-was-used"\necho unwanted\n' > "$TMPDIR/gui-askpass" + chmod +x "$TMPDIR/gui-askpass" + export SSH_ASKPASS="$TMPDIR/gui-askpass" + if printf 'protocol=https\nhost=git.example.invalid\npath=other.git\n\n' | git credential fill; then + echo 'Credentials leaked across repository paths' >&2; exit 1 + fi + test ! -e "$TMPDIR/gui-was-used" + printf 'protocol=https\nhost=git.example.invalid\npath=project.git\n\n' | git credential reject + if printf 'protocol=https\nhost=git.example.invalid\npath=project.git\n\n' | git credential fill; then + echo 'Rejected credentials remained cached' >&2; exit 1 + fi + # Exercise genuine /dev/tty entry too, without contacting a Git server. + export GIT_TERMINAL_PROMPT=1 + expect <<'EXPECT' + set timeout 10 + spawn -noecho git credential fill + send -- "protocol=https\rhost=terminal.example.invalid\rpath=project.git\r\r" + expect { + -exact "Username for 'https://terminal.example.invalid/project.git': " { send -- "terminal-user\r" } + timeout { exit 1 } + eof { exit 1 } + } + expect { + -exact "Password for 'https://terminal-user@terminal.example.invalid/project.git': " { send -- "offline-tty-token\r" } + timeout { exit 1 } + eof { exit 1 } + } + expect { + -exact "password=offline-tty-token" { } + timeout { exit 1 } + eof { exit 1 } + } + expect eof + lassign [wait] pid spawnid os_error status + exit $status + EXPECT + test ! -e "$TMPDIR/gui-was-used" + touch "$out" + '' diff --git a/tools.nix b/tools.nix index 0eaac19..c0c2e59 100644 --- a/tools.nix +++ b/tools.nix @@ -235,7 +235,22 @@ in programs.git = { enable = true; package = null; # The system module supplies Git. - settings.user.useConfigOnly = true; + settings = { + user.useConfigOnly = true; + core.askPass = ""; # Use /dev/tty, never fall back to SSH's GUI askpass. + credential = { + # Reset inherited helpers; keep secrets in memory, never plaintext files. + helper = [ + "" + "cache --timeout=31536000" + ]; # 365 days; cleared on reboot. + useHttpPath = true; # Don't reuse a repository token for unrelated paths. + }; + }; + }; + home.sessionVariables = { + GIT_ASKPASS = ""; + GIT_TERMINAL_PROMPT = "1"; }; programs.delta = {