Compare commits
7 Commits
4358918f3b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| b1564e73ca | |||
| 86c14ff05c | |||
| e6ca4f76e3 | |||
| 62e31f898d | |||
|
776578e770
|
|||
|
b29d9ce60d
|
|||
|
2ad7c1c03c
|
153
docs/git-1password-setup.md
Normal file
153
docs/git-1password-setup.md
Normal 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
|
||||
@@ -11,3 +11,4 @@ tree
|
||||
htop
|
||||
unzip
|
||||
zip
|
||||
jq
|
||||
|
||||
162
scripts/git-credential-1password.sh
Executable file
162
scripts/git-credential-1password.sh
Executable 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
|
||||
@@ -1,5 +1,6 @@
|
||||
# ~/.bash_aliases
|
||||
|
||||
alias ls='ls --color=auto'
|
||||
alias ll='ls -alF --color=auto'
|
||||
alias la='ls -A --color=auto'
|
||||
alias l='ls -CF --color=auto'
|
||||
@@ -30,3 +31,164 @@ 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'
|
||||
|
||||
alias dps='docker ps --format "table {{.ID}}\t{{.Names}}\t{{.Image}}\t{{.Status}}"'
|
||||
alias fzfdlogs='docker logs $(docker ps --format "{{.Names}}" | fzf)'
|
||||
alias fzfdrestart='docker restart $(docker ps --format "{{.Names}}" | fzf)'
|
||||
|
||||
# Password generator: passgen
|
||||
# Usage examples:
|
||||
# passgen # 24 chars, a-zA-Z0-9, 1 password
|
||||
# passgen -l 40 -c 5 # 5 passwords, length 40
|
||||
# passgen --symbols # include safe symbols for .env
|
||||
# passgen --no-upper # remove uppercase
|
||||
# passgen --exclude '$"' # exclude specific chars
|
||||
# passgen --ambiguous # remove ambiguous chars (O0Il1)
|
||||
# passgen --strict # enforce "at least one of each selected class"
|
||||
|
||||
passgen() {
|
||||
local length=24 count=1
|
||||
local use_upper=1 use_lower=1 use_digits=1 use_symbols=0
|
||||
local strict=0 ambiguous=0
|
||||
local exclude=""
|
||||
local safe_symbols="!@#%^&*()_+.-" # .env-friendly (no quotes, $, `, \, =, :, space)
|
||||
local symbol_set="$safe_symbols" # can be extended later if you want a "wide" set
|
||||
|
||||
# parse args
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-l|--length) length="${2:-}"; shift 2 ;;
|
||||
-c|--count) count="${2:-}"; shift 2 ;;
|
||||
--no-upper) use_upper=0; shift ;;
|
||||
--no-lower) use_lower=0; shift ;;
|
||||
--no-digits) use_digits=0; shift ;;
|
||||
--symbols) use_symbols=1; shift ;;
|
||||
--no-symbols) use_symbols=0; shift ;;
|
||||
--exclude) exclude="${2:-}"; shift 2 ;;
|
||||
--ambiguous) ambiguous=1; shift ;;
|
||||
--strict) strict=1; shift ;;
|
||||
-h|--help)
|
||||
cat <<'EOF'
|
||||
passgen - flexible password generator
|
||||
|
||||
Options:
|
||||
-l, --length N Length of each password (default 24)
|
||||
-c, --count N How many passwords to generate (default 1)
|
||||
--no-upper Exclude A-Z
|
||||
--no-lower Exclude a-z
|
||||
--no-digits Exclude 0-9
|
||||
--symbols Include .env-safe symbols (!@#%^&*()_+.-)
|
||||
--no-symbols Exclude symbols (default)
|
||||
--exclude "xyz" Remove these characters from output
|
||||
--ambiguous Remove ambiguous characters: O 0 I l 1
|
||||
--strict Ensure each selected class appears at least once
|
||||
-h, --help Show this help
|
||||
|
||||
Notes:
|
||||
- Default charset: a-z A-Z 0-9
|
||||
- Symbols are .env-safe by default (no quotes, $, `, \, =, :, space)
|
||||
EOF
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "passgen: unknown option: $1" >&2; return 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# sanity checks
|
||||
[[ "$length" =~ ^[0-9]+$ ]] || { echo "passgen: length must be a number" >&2; return 2; }
|
||||
[[ "$count" =~ ^[0-9]+$ ]] || { echo "passgen: count must be a number" >&2; return 2; }
|
||||
|
||||
# build class strings explicitly (no tr bracket weirdness)
|
||||
local UPPER="ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
local LOWER="abcdefghijklmnopqrstuvwxyz"
|
||||
local DIGIT="0123456789"
|
||||
local SYMS="$symbol_set"
|
||||
|
||||
# optionally remove ambiguous chars
|
||||
if (( ambiguous )); then
|
||||
UPPER=${UPPER//O/}
|
||||
UPPER=${UPPER//I/}
|
||||
LOWER=${LOWER//l/}
|
||||
DIGIT=${DIGIT//0/}
|
||||
DIGIT=${DIGIT//1/}
|
||||
fi
|
||||
|
||||
# compose allowed set
|
||||
local allowed=""
|
||||
(( use_upper )) && allowed+="$UPPER"
|
||||
(( use_lower )) && allowed+="$LOWER"
|
||||
(( use_digits )) && allowed+="$DIGIT"
|
||||
(( use_symbols ))&& allowed+="$SYMS"
|
||||
|
||||
# if nothing selected, default to alnum
|
||||
if [[ -z "$allowed" ]]; then
|
||||
allowed="$UPPER$LOWER$DIGIT"
|
||||
fi
|
||||
|
||||
# remove any user-excluded chars from allowed (do this via tr to avoid globbing issues)
|
||||
if [[ -n "$exclude" ]]; then
|
||||
# printf each char of allowed, delete excluded via tr, then reassemble
|
||||
allowed="$(printf %s "$allowed" | tr -d "$exclude")"
|
||||
fi
|
||||
|
||||
# final check
|
||||
if [[ -z "$allowed" ]]; then
|
||||
echo "passgen: allowed set is empty after exclusions; loosen options." >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
# helper: generate one password meeting optional strict policy
|
||||
_gen_one() {
|
||||
local need_upper=$use_upper
|
||||
local need_lower=$use_lower
|
||||
local need_digit=$use_digits
|
||||
local need_symbol=$use_symbols
|
||||
local out=""
|
||||
|
||||
# seed with at least one of each selected class if --strict
|
||||
if (( strict )); then
|
||||
if (( need_upper )); then out+="${UPPER:RANDOM%${#UPPER}:1}"; fi
|
||||
if (( need_lower )); then out+="${LOWER:RANDOM%${#LOWER}:1}"; fi
|
||||
if (( need_digit )); then out+="${DIGIT:RANDOM%${#DIGIT}:1}"; fi
|
||||
if (( need_symbol )); then out+="${SYMS:RANDOM%${#SYMS}:1}"; fi
|
||||
fi
|
||||
|
||||
# fill the rest from the full allowed set, using /dev/urandom
|
||||
local needed=$(( length - ${#out} ))
|
||||
if (( needed > 0 )); then
|
||||
# draw more than needed, then cut (helps when exclusions reduce size)
|
||||
local draw=$(( needed * 3 ))
|
||||
local extra="$(LC_ALL=C tr -dc "$allowed" </dev/urandom | head -c "$draw")"
|
||||
out+="${extra:0:needed}"
|
||||
fi
|
||||
|
||||
# If exclusions or strict left us short (very rare), top-up in a loop.
|
||||
while (( ${#out} < length )); do
|
||||
out+=$(LC_ALL=C tr -dc "$allowed" </dev/urandom | head -c 1)
|
||||
done
|
||||
|
||||
# FisherYates shuffle so strict-class seeds aren't predictable up front
|
||||
local i j tmp arr=()
|
||||
for (( i=0; i<length; i++ )); do arr[i]="${out:i:1}"; done
|
||||
for (( i=length-1; i>0; i-- )); do
|
||||
# get a random index j using bytes from /dev/urandom
|
||||
j=$(( $(od -An -N2 -tu2 < /dev/urandom) % (i+1) ))
|
||||
tmp=${arr[i]}; arr[i]=${arr[j]}; arr[j]=$tmp
|
||||
done
|
||||
printf "%s" "${arr[*]}" | tr -d ' '
|
||||
}
|
||||
|
||||
# generate the requested count
|
||||
local i
|
||||
for (( i=1; i<=count; i++ )); do
|
||||
_gen_one
|
||||
printf "\n"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -14,3 +14,5 @@ format = ssh
|
||||
program = /opt/1Password/op-ssh-sign
|
||||
[commit]
|
||||
gpgsign = true
|
||||
[credential]
|
||||
helper = !~/.dotfiles/scripts/git-credential-1password.sh
|
||||
|
||||
Reference in New Issue
Block a user