From 2ad7c1c03c2aa80cb3f265767843dd7a322edad5 Mon Sep 17 00:00:00 2001 From: Ryan Hamilton Date: Thu, 7 Aug 2025 07:50:09 -0500 Subject: [PATCH] feat: add Git and 1Password credential helper and update configurations --- docs/git-1password-setup.md | 130 ++++++++++++++++++++++++ packages/base.txt | 2 + scripts/git-credential-1password.sh | 150 ++++++++++++++++++++++++++++ stow/bash/.bash_aliases | 4 + stow/git/.gitconfig | 2 + 5 files changed, 288 insertions(+) create mode 100644 docs/git-1password-setup.md create mode 100755 scripts/git-credential-1password.sh diff --git a/docs/git-1password-setup.md b/docs/git-1password-setup.md new file mode 100644 index 0000000..2148304 --- /dev/null +++ b/docs/git-1password-setup.md @@ -0,0 +1,130 @@ +# Git + 1Password HTTPS Credential Helper + +This setup allows Git to automatically fetch HTTPS credentials from 1Password without storing them locally. + +## Prerequisites + +1. **1Password CLI installed**: The `op` command should be available + + - On Ubuntu/Debian: Install from 1Password's official repository + - Package name: `1password-cli` (included in base.txt) + +2. **1Password CLI authenticated**: You must be signed in to 1Password CLI + + ```bash + op signin + ``` + +3. **jq installed**: For JSON parsing (included in base.txt) + +## Setup + +The credential helper is automatically configured in your `.gitconfig`: + +```ini +[credential] +helper = !~/.dotfiles/scripts/git-credential-1password.sh +``` + +## Usage + +### Storing Credentials in 1Password + +For each Git HTTPS remote you want to use, create an item in 1Password with: + +1. **Title**: Include the hostname (e.g., "GitHub", "gitlab.example.com", "bitbucket.org") +2. **Username field**: Your Git username +3. **Password field**: Your Git password/token +4. **URL field** (optional but recommended): The full HTTPS URL of the repository + +#### Examples: + +**GitHub Personal Access Token:** + +- Title: "GitHub" +- Username: your-github-username +- Password: ghp_xxxxxxxxxxxxxxxxxxxx +- URL: https://github.com + +**GitLab Token:** + +- Title: "gitlab.example.com" +- Username: your-gitlab-username +- Password: glpat-xxxxxxxxxxxxxxxxxxxx +- URL: https://gitlab.example.com + +### Using with Git + +Once set up, Git operations will automatically prompt 1Password for credentials: + +```bash +# Clone a private repo +git clone https://github.com/user/private-repo.git + +# Push to origin +git push origin main + +# Add a new HTTPS remote +git remote add upstream https://github.com/upstream/repo.git +``` + +## How It Works + +1. When Git needs HTTPS credentials, it calls the credential helper +2. The helper searches 1Password for items matching the hostname +3. It looks for matches in: + - URL fields containing the hostname + - Item titles containing the hostname + - Additional information containing the hostname +4. Returns the username and password to Git +5. Git uses these credentials for the operation + +## Troubleshooting + +### "1Password CLI (op) not found" + +Install 1Password CLI or ensure it's in your PATH: + +```bash +# Check if installed +which op + +# Install if missing (Ubuntu/Debian) +curl -sS https://downloads.1password.com/linux/keys/1password.asc | sudo gpg --dearmor --output /usr/share/keyrings/1password-archive-keyring.gpg +echo 'deb [arch=amd64 signed-by=/usr/share/keyrings/1password-archive-keyring.gpg] https://downloads.1password.com/linux/debian/amd64 stable main' | sudo tee /etc/apt/sources.list.d/1password.list +sudo apt update && sudo apt install 1password-cli +``` + +### "Not signed in to 1Password CLI" + +Sign in to 1Password CLI: + +```bash +op signin +``` + +### "No matching item found" + +- Ensure the 1Password item title or URL contains the Git hostname +- Check that the item has username and password fields +- Try creating a new item with a clear title matching the hostname + +### Test the Helper Manually + +```bash +# Test the credential helper directly +echo -e "protocol=https\nhost=github.com\n" | ~/.dotfiles/scripts/git-credential-1password.sh get +``` + +## Security Benefits + +- Credentials are never stored in plain text on disk +- Works with 1Password's security features (Touch ID, master password, etc.) +- Credentials are fetched fresh each time (no caching) +- Works seamlessly with existing 1Password setup + +## Limitations + +- Only works with HTTPS Git remotes (SSH remotes continue to use SSH keys) +- Requires 1Password CLI to be signed in +- May prompt for 1Password unlock depending on your security settings diff --git a/packages/base.txt b/packages/base.txt index 8131853..df95651 100644 --- a/packages/base.txt +++ b/packages/base.txt @@ -11,3 +11,5 @@ tree htop unzip zip +jq +1password-cli diff --git a/scripts/git-credential-1password.sh b/scripts/git-credential-1password.sh new file mode 100755 index 0000000..ee26a07 --- /dev/null +++ b/scripts/git-credential-1password.sh @@ -0,0 +1,150 @@ +#!/bin/bash +# Git credential helper for 1Password CLI +# This script integrates with Git's credential system to fetch credentials from 1Password + +set -euo pipefail + +# Source utilities for logging +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [ -f "$SCRIPT_DIR/utils.sh" ]; then + source "$SCRIPT_DIR/utils.sh" +fi + +# Function to check if 1Password CLI is available and signed in +check_op_cli() { + if ! command -v op >/dev/null 2>&1; then + echo "Error: 1Password CLI (op) not found. Please install it first." >&2 + return 1 + fi + + # Check if signed in + if ! op account list >/dev/null 2>&1; then + echo "Error: Not signed in to 1Password CLI. Please run 'op signin' first." >&2 + return 1 + fi + + return 0 +} + +# Parse Git credential input +parse_input() { + while IFS= read -r line; do + if [ -z "$line" ]; then + break + fi + + case "$line" in + protocol=*) + protocol="${line#protocol=}" + ;; + host=*) + host="${line#host=}" + ;; + username=*) + username="${line#username=}" + ;; + password=*) + password="${line#password=}" + ;; + esac + done +} + +# Get credentials from 1Password +get_credentials() { + local search_term="$1" + + # Try to find item by URL/host first + local item_uuid + if ! item_uuid=$(op item list --format=json 2>/dev/null | jq -r --arg host "$search_term" ' + .[] | select( + (.urls[]?.href // "" | test($host; "i")) or + (.title | test($host; "i")) or + (.additional_information | test($host; "i")) + ) | .id' | head -1); then + return 1 + fi + + if [ -z "$item_uuid" ]; then + # Fallback: search by title containing the host + if ! item_uuid=$(op item list --format=json 2>/dev/null | jq -r --arg host "$search_term" ' + .[] | select(.title | test($host; "i")) | .id' | head -1); then + return 1 + fi + fi + + if [ -z "$item_uuid" ]; then + echo "No matching item found in 1Password for: $search_term" >&2 + return 1 + fi + + # Get the item details + local item_json + if ! item_json=$(op item get "$item_uuid" --format=json 2>/dev/null); then + echo "Failed to retrieve item from 1Password" >&2 + return 1 + fi + + # Extract username and password + local op_username op_password + op_username=$(echo "$item_json" | jq -r '.fields[] | select(.id == "username" or .label == "username") | .value // empty' | head -1) + op_password=$(echo "$item_json" | jq -r '.fields[] | select(.id == "repo_token" or .label == "repo_token") | .value // empty' | head -1) + + if [ -z "$op_username" ] || [ -z "$op_password" ]; then + echo "Username or password not found in 1Password item" >&2 + return 1 + fi + + echo "username=$op_username" + echo "password=$op_password" +} + +# Main credential helper logic +case "${1:-}" in + get) + # Initialize variables + protocol="" + host="" + username="" + password="" + + # Parse input from Git + parse_input + + # Only handle HTTPS requests + if [ "$protocol" != "https" ]; then + exit 0 + fi + + # Check 1Password CLI availability + if ! check_op_cli; then + exit 1 + fi + + # Search for credentials + if [ -n "$host" ]; then + if get_credentials "$host"; then + exit 0 + fi + fi + + # If we get here, no credentials were found + exit 1 + ;; + + store) + # We don't store credentials in 1Password via this helper + # Users should add them manually to 1Password + exit 0 + ;; + + erase) + # We don't erase credentials from 1Password via this helper + exit 0 + ;; + + *) + echo "Usage: $0 {get|store|erase}" >&2 + exit 1 + ;; +esac diff --git a/stow/bash/.bash_aliases b/stow/bash/.bash_aliases index 88c50da..0a1b86b 100644 --- a/stow/bash/.bash_aliases +++ b/stow/bash/.bash_aliases @@ -30,3 +30,7 @@ alias install_neofetch='sudo apt install -y neofetch' alias memoryinfo='sudo dmidecode -t memory | grep -i "Type:\|Speed:\|Size:"' alias install_packages='bash ~/.dotfiles/packages/install.sh ~/.dotfiles' alias update_list='sudo apt update && sudo apt list --upgradable' + +# Git + 1Password helpers +alias test_git_1p='echo "Testing Git 1Password credential helper for github.com:" && echo -e "protocol=https\nhost=github.com\n" | ~/.dotfiles/scripts/git-credential-1password.sh get' +alias op_signin='op signin' diff --git a/stow/git/.gitconfig b/stow/git/.gitconfig index 18af3d7..411d292 100644 --- a/stow/git/.gitconfig +++ b/stow/git/.gitconfig @@ -14,3 +14,5 @@ format = ssh program = /opt/1Password/op-ssh-sign [commit] gpgsign = true +[credential] +helper = !~/.dotfiles/scripts/git-credential-1password.sh