1
0

Compare commits

...

3 Commits

6 changed files with 328 additions and 0 deletions

153
docs/git-1password-setup.md Normal file
View File

@@ -0,0 +1,153 @@
# 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
# Debug: List all 1Password items to see what's available
op item list --format=json | jq -r '.[] | "\(.title) - \(.id)"'
# Debug: See the structure of a specific item
op item get "YOUR_ITEM_ID" --format=json | jq
# Debug: Check what fields are available in an item
op item get "YOUR_ITEM_ID" --format=json | jq -r '.fields[] | "\(.label // .id): \(.value // "empty")"'
```
### Common Issues
**jq null matching errors:**
- This happens when 1Password items have missing fields
- The updated script handles null values gracefully
- Make sure your items have proper username and password fields
**Field naming issues:**
- The script looks for fields with labels containing: "username", "user", "login"
- For passwords, it looks for: "password", "token", "secret", "pass"
- If your fields have different names, rename them in 1Password
## 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

View File

@@ -11,3 +11,5 @@ tree
htop
unzip
zip
jq
1password-cli

View File

@@ -0,0 +1,162 @@
#!/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 // [])[] | select(.href != null and (.href | type) == "string") | .href | test($host; "i")) or
((.title // "") | type == "string" and test($host; "i")) or
((.additional_information // "") | type == "string" and test($host; "i"))
) | .id' | head -1 2>/dev/null); then
# If the complex search fails, try a simpler approach
item_uuid=""
fi
if [ -z "$item_uuid" ]; then
# Fallback: simple 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 // "") != "" and ((.title // "") | type == "string") and ((.title // "") | test($host; "i"))) | .id' | head -1 2>/dev/null); then
item_uuid=""
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 // "") | type == "string" and test("username"; "i"))) | .value // empty' | head -1 2>/dev/null)
op_password=$(echo "$item_json" | jq -r '(.fields // [])[] | select((.id // "") == "password" or ((.label // "") | type == "string" and test("password|token"; "i"))) | .value // empty' | head -1 2>/dev/null)
# If standard fields don't work, try common field names
if [ -z "$op_username" ]; then
op_username=$(echo "$item_json" | jq -r '(.fields // [])[] | select((.label // "") | type == "string" and test("user|login"; "i")) | .value // empty' | head -1 2>/dev/null)
fi
if [ -z "$op_password" ]; then
op_password=$(echo "$item_json" | jq -r '(.fields // [])[] | select((.label // "") | type == "string" and test("pass|token|secret"; "i")) | .value // empty' | head -1 2>/dev/null)
fi
if [ -z "$op_username" ] || [ -z "$op_password" ]; then
echo "Username or password not found in 1Password item" >&2
echo "Available fields:" >&2
echo "$item_json" | jq -r '(.fields // [])[] | " - \(.label // .id // "unknown")"' 2>/dev/null >&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

View File

@@ -30,3 +30,9 @@ 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'
alias op_list='op item list --format=json | jq -r ".[] | \"\(.title) - \(.id)\""'
alias op_debug_item='function _op_debug() { op item get "$1" --format=json | jq -r ".fields[] | \"\(.label // .id): \(.value // \"empty\")\""; }; _op_debug'

View File

@@ -23,3 +23,6 @@ shopt -s histappend
if [ -f /etc/bash_completion ]; then
. /etc/bash_completion
fi
export SSH_AUTH_SOCK="/run/user/$UID/1password/agent.sock"

View File

@@ -14,3 +14,5 @@ format = ssh
program = /opt/1Password/op-ssh-sign
[commit]
gpgsign = true
[credential]
helper = !~/.dotfiles/scripts/git-credential-1password.sh