Skip to content

Commit 4e34ea0

Browse files
committed
init
0 parents  commit 4e34ea0

15 files changed

Lines changed: 985 additions & 0 deletions

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
pull_request:
6+
7+
jobs:
8+
lint:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- name: Checkout
12+
uses: actions/checkout@v4
13+
14+
- name: Install shellcheck
15+
run: sudo apt-get update && sudo apt-get install -y shellcheck
16+
17+
- name: Run lint
18+
run: tests/lint.sh

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Local/private reference docs (keep folder, ignore contents)
2+
docs/local-only/*
3+
!docs/local-only/.gitkeep
4+
5+
# Backward-compatible direct path ignore
6+
docs/ssh_multi_account_setup.md

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# Changelog
2+
3+
## 0.1.0 - 2026-02-14
4+
5+
- Initial release.
6+
- Added interactive `scripts/setup.sh` with dry-run default.
7+
- Added `scripts/update-remotes.sh` for owner-to-alias URL migration.
8+
- Added key helper scripts and backup script.
9+
- Added examples for owner mapping and git `includeIf`.
10+
- Added docs and lint workflow.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# SSH Multi-Account Setup (macOS + GitHub)
2+
3+
Safe, script-first tooling to manage multiple GitHub accounts on one machine with separate SSH keys, host aliases, and dry-run defaults.
4+
5+
## Features
6+
7+
- Audits and backs up existing `~/.ssh` state before changes.
8+
- Generates separate Ed25519 keys per account.
9+
- Adds keys to `ssh-agent` and macOS keychain.
10+
- Optionally uploads keys through `gh` after explicit confirmation.
11+
- Writes a managed block in `~/.ssh/config` with account aliases.
12+
- Validates aliases using `ssh -T git@<alias>`.
13+
- Updates existing repo remotes with a safe dry-run workflow.
14+
15+
## Repository Layout
16+
17+
- `scripts/setup.sh`: Main idempotent setup flow.
18+
- `scripts/update-remotes.sh`: Convert remotes to alias-based SSH URLs.
19+
- `scripts/backup-keys.sh`: Back up SSH files.
20+
- `scripts/generate-key.sh`: Standalone key creation helper.
21+
- `scripts/generate-gh-actions-key.sh`: Optional CI key helper.
22+
- `examples/owner-map.conf.example`: Owner-to-alias mapping example.
23+
- `examples/gitconfig-includeIf.example`: Git identity routing example.
24+
- `docs/audit-and-verify.md`: Manual audit and verification steps.
25+
- `tests/lint.sh`: Shell lint script.
26+
27+
## Prerequisites
28+
29+
- macOS with OpenSSH
30+
- Bash 4+
31+
- `git`
32+
- Optional: `gh` (GitHub CLI) for key upload
33+
- Optional: `shellcheck` for lint checks
34+
35+
## Quick Start
36+
37+
```bash
38+
chmod +x scripts/*.sh tests/lint.sh
39+
scripts/setup.sh --dry-run
40+
scripts/setup.sh --apply
41+
```
42+
43+
Interactive defaults:
44+
45+
- accounts: `personal,work`
46+
- key names: `id_ed25519_personal`, `id_ed25519_work`
47+
- aliases: `github-personal`, `github-work`
48+
49+
Explicit example:
50+
51+
```bash
52+
scripts/setup.sh --apply \
53+
--accounts "personal,work" \
54+
--email-personal "you@personal.email" \
55+
--email-work "you@work.email"
56+
```
57+
58+
Non-interactive example:
59+
60+
```bash
61+
scripts/setup.sh --apply --yes \
62+
--accounts "personal,work" \
63+
--email-personal "you@personal.email" \
64+
--email-work "you@work.email" \
65+
--key-personal "id_ed25519_personal" \
66+
--key-work "id_ed25519_work" \
67+
--alias-personal "github-personal" \
68+
--alias-work "github-work"
69+
```
70+
71+
## Update Remotes
72+
73+
Dry-run by default:
74+
75+
```bash
76+
scripts/update-remotes.sh --root "$HOME/code" --map examples/owner-map.conf.example --dry-run
77+
```
78+
79+
Apply changes (with confirmation):
80+
81+
```bash
82+
scripts/update-remotes.sh --root "$HOME/code" --map ~/.ssh/owner-map.conf --apply
83+
```
84+
85+
## Manual Commands (Reference)
86+
87+
Audit:
88+
89+
```bash
90+
ls -la ~/.ssh
91+
ls -1 ~/.ssh/*.pub 2>/dev/null || echo "no .pub files found"
92+
ssh-add -l || echo "no keys loaded"
93+
gh ssh-key list
94+
```
95+
96+
Backup:
97+
98+
```bash
99+
mkdir -p ~/ssh-backups
100+
cp -v ~/.ssh/id_* ~/ssh-backups/ 2>/dev/null || echo "copied any id_* keys"
101+
ls -la ~/ssh-backups
102+
```
103+
104+
Generate keys:
105+
106+
```bash
107+
ssh-keygen -t ed25519 -C "you@personal.email" -f ~/.ssh/id_ed25519_personal
108+
ssh-keygen -t ed25519 -C "you@work.email" -f ~/.ssh/id_ed25519_work
109+
```
110+
111+
Add to agent/keychain:
112+
113+
```bash
114+
eval "$(ssh-agent -s)"
115+
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_personal
116+
ssh-add --apple-use-keychain ~/.ssh/id_ed25519_work
117+
ssh-add -l
118+
```
119+
120+
Upload with `gh` (optional):
121+
122+
```bash
123+
gh auth login
124+
gh ssh-key add ~/.ssh/id_ed25519_personal.pub --title "MacBook Personal $(date +%F)"
125+
gh auth login
126+
gh ssh-key add ~/.ssh/id_ed25519_work.pub --title "MacBook Work $(date +%F)"
127+
```
128+
129+
Use aliases in remotes:
130+
131+
```bash
132+
git remote set-url origin git@github-personal:your-personal-username/repo.git
133+
git remote set-url origin git@github-work:your-work-username/repo.git
134+
```
135+
136+
Validate:
137+
138+
```bash
139+
ssh -T git@github-personal
140+
ssh -T git@github-work
141+
ssh -vT git@github-personal 2>&1 | sed -n '1,200p'
142+
```
143+
144+
## Safety Rules
145+
146+
- Never delete keys silently.
147+
- Keep backups until all fetch/push checks are successful.
148+
- `update-remotes.sh` is dry-run by default and confirms before apply.
149+
- `setup.sh` prompts before risky operations unless `--yes` is used.
150+
151+
## Verification Checklist
152+
153+
- [ ] `ssh -T git@github-personal` authenticates to the personal username.
154+
- [ ] `ssh -T git@github-work` authenticates to the work username.
155+
- [ ] Critical repositories can fetch and push.
156+
- [ ] `gh ssh-key list` reflects intended keys.
157+
- [ ] Backups exist under `~/ssh-backups/<timestamp>`.
158+
159+
## Caution About Old Keys
160+
161+
Only remove old keys after successful verification across all repos. Prefer staged cleanup and revoke old keys in GitHub first.

docs/audit-and-verify.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Audit and Verify
2+
3+
## Local Audit
4+
5+
```bash
6+
ls -la ~/.ssh
7+
ls -1 ~/.ssh/*.pub 2>/dev/null || echo "no .pub files found"
8+
ssh-add -l || echo "no keys loaded"
9+
```
10+
11+
If GitHub CLI is installed and authenticated:
12+
13+
```bash
14+
gh ssh-key list
15+
```
16+
17+
## Backups
18+
19+
```bash
20+
scripts/backup-keys.sh --dry-run
21+
scripts/backup-keys.sh --apply
22+
```
23+
24+
## Verify Aliases
25+
26+
```bash
27+
ssh -T git@github-personal
28+
ssh -T git@github-work
29+
```
30+
31+
Expected pattern:
32+
33+
- `Hi <username>! You've successfully authenticated, but GitHub does not provide shell access.`
34+
35+
## Verify Repository Access
36+
37+
For representative repositories:
38+
39+
```bash
40+
git fetch
41+
git push --dry-run
42+
```
43+
44+
## Only Then: Key Cleanup
45+
46+
- Revoke old keys on GitHub.
47+
- Remove old local keys with explicit commands and prompts.
48+
- Keep backup snapshots until all workflows remain stable.

docs/local-only/.gitkeep

Whitespace-only changes.
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# ~/.gitconfig
2+
3+
[includeIf "gitdir:~/code/personal/"]
4+
path = ~/.gitconfig-personal
5+
6+
[includeIf "gitdir:~/code/work/"]
7+
path = ~/.gitconfig-work
8+
9+
# ~/.gitconfig-personal
10+
[user]
11+
name = Your Name
12+
email = you@personal.email
13+
14+
# ~/.gitconfig-work
15+
[user]
16+
name = Your Work Name
17+
email = you@work.email

examples/owner-map.conf.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# owner=alias
2+
your-personal-username=github-personal
3+
your-work-org=github-work

scripts/backup-keys.sh

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
usage() {
5+
cat <<USAGE
6+
Usage: $(basename "$0") [--dry-run] [--yes] [--dest DIR]
7+
8+
Back up SSH key material from ~/.ssh to ~/ssh-backups/<timestamp> by default.
9+
USAGE
10+
}
11+
12+
DRY_RUN=true
13+
ASSUME_YES=false
14+
DEST_BASE="$HOME/ssh-backups"
15+
16+
while [[ $# -gt 0 ]]; do
17+
case "$1" in
18+
--dry-run) DRY_RUN=true ;;
19+
--apply) DRY_RUN=false ;;
20+
--yes|--non-interactive) ASSUME_YES=true ;;
21+
--dest)
22+
shift
23+
DEST_BASE="${1:-}"
24+
[[ -n "$DEST_BASE" ]] || { echo "Missing value for --dest" >&2; exit 1; }
25+
;;
26+
-h|--help)
27+
usage
28+
exit 0
29+
;;
30+
*)
31+
echo "Unknown argument: $1" >&2
32+
usage
33+
exit 1
34+
;;
35+
esac
36+
shift
37+
done
38+
39+
TS=$(date +%Y%m%d-%H%M%S)
40+
DEST_DIR="$DEST_BASE/$TS"
41+
SOURCE_DIR="$HOME/.ssh"
42+
43+
if [[ ! -d "$SOURCE_DIR" ]]; then
44+
echo "No ~/.ssh directory found at $SOURCE_DIR"
45+
exit 0
46+
fi
47+
48+
FILES=()
49+
while IFS= read -r line; do
50+
FILES+=("$line")
51+
done < <(find "$SOURCE_DIR" -maxdepth 1 -type f \( -name 'id_*' -o -name '*.pub' -o -name 'config' -o -name 'known_hosts*' \) | sort)
52+
53+
if [[ ${#FILES[@]} -eq 0 ]]; then
54+
echo "No matching SSH files found to back up."
55+
exit 0
56+
fi
57+
58+
echo "Backup destination: $DEST_DIR"
59+
echo "Files to backup:"
60+
printf ' %s\n' "${FILES[@]}"
61+
62+
if [[ "$DRY_RUN" == true ]]; then
63+
echo "[DRY RUN] No files copied."
64+
exit 0
65+
fi
66+
67+
if [[ "$ASSUME_YES" != true ]]; then
68+
read -r -p "Proceed with backup? [y/N] " reply
69+
[[ "$reply" =~ ^[Yy]$ ]] || { echo "Cancelled."; exit 1; }
70+
fi
71+
72+
mkdir -p "$DEST_DIR"
73+
for f in "${FILES[@]}"; do
74+
cp -p "$f" "$DEST_DIR/"
75+
done
76+
77+
echo "Backup complete: $DEST_DIR"

0 commit comments

Comments
 (0)