Compare commits

...
10 Commits
Author SHA1 Message Date
edemilovandClaude Sonnet 5 0480c7aed5 Merge sshinstall: add cross-platform SSH bootstrap scripts
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 15:30:44 +03:00
edemilovandClaude Sonnet 5 337a209713 fix: final review fixes for sshinstall (elevation guard, Win8 drop, authorized_keys newline, style)
Final whole-branch review pass before merge:

1. sshinstall.ps1: #Requires -RunAsAdministrator doesn't enforce under
   `irm ... | iex` (piping into Invoke-Expression). Add an explicit
   WindowsPrincipal/IsInRole elevation check before anything else runs,
   so a non-elevated run fails fast instead of silently falling through
   Get-WindowsCapability's -ErrorAction SilentlyContinue into the
   unsupported-OS branch.

2. sshinstall.ps1: drop the Windows 8 portable Win32-OpenSSH fallback —
   it depends on Expand-Archive (PS 5.0+) and implicit TLS 1.2, neither
   present on stock Windows 8's PS 3.0/4.0, so the one path that existed
   for Windows 8 was guaranteed to fail on Windows 8. Replaced with a
   clear unsupported-OS failure. Windows support is now 10 (1809+)/11
   only; README table updated to match.

3. sshinstall.sh: guarantee a trailing newline before appending the
   admin key to authorized_keys, so a pre-existing file without one
   doesn't get its last line glued to the new key (which also defeats
   the grep -qF idempotency check on every future run). Added the
   equivalent guard to sshinstall.ps1's Add-Content path too — its
   WriteLine only guarantees a trailing newline after what it writes,
   not before, so a hand-edited or externally-written
   administrators_authorized_keys has the same exposure.

4. sshinstall.sh: pacman -Sy -> -S (drop the partial-upgrade footgun,
   matches apps.sh's install-vs-explicit-update convention); chown -R
   now sets both user and group explicitly (matches apps.sh/
   netdrivelink.sh convention).

5. sshinstall.ps1: rename the firewall rule this script creates to
   OpenSSH-Server-In-TCP-sshinstall — the previous name collided with
   the exact rule name Microsoft's own Add-WindowsCapability OpenSSH
   install uses, so a disabled Microsoft-created rule of that name
   would cause New-NetFirewallRule to throw under
   $ErrorActionPreference = "Stop" and kill the script before the key
   was deployed.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 15:27:30 +03:00
edemilovandClaude Sonnet 5 93d9ead905 Correct spec after final review: drop unworkable Windows 8 fallback, fix stale branch/key-destination wording
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 15:24:34 +03:00
edemilov c4efd37831 docs: add sshinstall.sh/.ps1 usage 2026-09-17 15:17:14 +03:00
edemilovandClaude Haiku 4.5 def3d57799 fix: sshinstall.ps1 ACL idempotency + SID hardening + Move-Item safety
- Fix ACL idempotency: check if ACLs already locked down before running
  icacls; use Write-Skip if no broader groups present, else re-lock
- Harden SID check: explicit SecurityIdentifier construction vs string coercion
- Add explicit New-Item for Move-Item destination in Win8 fallback

Co-Authored-By: Claude Haiku 4.5 <[email protected]>
2026-09-17 15:14:28 +03:00
edemilovandClaude Haiku 4.5 902e95a79d Add sshinstall.ps1 — Windows SSH bootstrap + admin key deploy
Co-Authored-By: Claude Haiku 4.5 <[email protected]>
2026-09-17 15:07:40 +03:00
edemilov 088d692954 Add sshinstall.sh — Linux SSH bootstrap + admin key deploy 2026-09-17 15:04:10 +03:00
edemilovandClaude Sonnet 5 25800331fe Add implementation plan for sshinstall bootstrap scripts
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 14:58:04 +03:00
edemilovandClaude Sonnet 5 2a2ed5711b Fill in admin public key in sshinstall design spec
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 14:55:16 +03:00
edemilovandClaude Sonnet 5 9a68a19dea Add design spec for cross-platform sshinstall bootstrap script
Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-09-17 14:51:41 +03:00
5 changed files with 904 additions and 0 deletions

No files matched your search

+8
View File
@@ -13,6 +13,12 @@ bash <(curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/main/netdrive
# Windows (PowerShell as Admin)
irm https://git.edemilov.ru/edemilov/scripts/raw/branch/main/apps.ps1 | iex
# Bootstrap SSH access on a new machine (installs+enables sshd, deploys admin key)
curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.sh | sudo bash
# Windows (PowerShell as Admin)
irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
```
## Scripts
@@ -22,6 +28,8 @@ irm https://git.edemilov.ru/edemilov/scripts/raw/branch/main/apps.ps1 | iex
| `apps.sh` | Install common dev/security/daily tools | Arch, Parrot, Kali, Debian, Ubuntu |
| `netdrivelink.sh` | Mount vanga Samba shares + 2nd drive symlinks | Same |
| `apps.ps1` | Windows equivalent via winget + map network drives | Windows 10/11 |
| `sshinstall.sh` | Install/enable SSH server + deploy admin key | Arch, Debian, Ubuntu, RHEL, Fedora |
| `sshinstall.ps1` | Windows equivalent | Windows 10, 11 |
| `gitea-ct-setup.sh` | Create CT116 with Gitea on Proxmox host | Proxmox 8+ |
## Samba shares (vanga — 10.0.99.100)
@@ -0,0 +1,471 @@
# sshinstall Bootstrap Scripts Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Add `sshinstall.sh` (Linux) and `sshinstall.ps1` (Windows) to the
`git.edemilov.ru/edemilov/Scripts` repo — a curl/iwr-pipeable pair that installs/enables
an SSH server and deploys a known admin public key into `authorized_keys`, so the first
connection afterward is already passwordless.
**Architecture:** Two independent, self-contained scripts (no shared file — Windows can't
source bash), each following the exact style/structure already established by this
repo's `apps.sh`/`apps.ps1` pair: colored logging helpers, a detect → install → enable →
firewall → done pipeline, idempotent at every step.
**Tech Stack:** Bash (Linux, `apt`/`pacman`/`dnf`/`yum`) and PowerShell 5+ (Windows,
native `Add-WindowsCapability` with a portable-binary fallback for pre-10 systems). No
CI/test harness in this repo — verification is real-machine manual testing, matching
`apps.sh`'s own testing approach.
## Global Constraints
- Full spec: `docs/superpowers/specs/2026-09-17-sshinstall-design.md` — read it before
starting.
- The admin public key to embed in **both** scripts, verbatim, exactly once each:
```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop
```
- No self-elevation. Linux: check `$EUID -eq 0` and `die` with a one-line message if not.
Windows: `#Requires -RunAsAdministrator` (PowerShell itself refuses to run and prints
its own error if not elevated — do not add a second manual check).
- No `PasswordAuthentication no` / any `sshd_config` hardening — password auth stays
enabled. Out of scope entirely for this plan.
- Every step must be idempotent: check current state before acting, and use the
`skip`/`Write-Skip` helper (not `info`/`Write-Info`) to report "already done" instead
of re-doing work or silently duplicating a line (especially the key line in
`authorized_keys` — must never be appended twice on a re-run).
- Match `apps.sh`'s exact logging-helper style (`info`/`success`/`warn`/`skip`/`die`/
`header`, same color codes) and `apps.ps1`'s exact PowerShell equivalents
(`Write-Info`/`Write-Success`/`Write-Skip`/`Write-Warn`/`Write-Header`) — copy them
verbatim from those two files rather than reinventing new ones.
- Repo has no push access this session (SSH key to `git@10.0.99.116` is locked) — commit
locally on `master`, push is a separate step once the key is unlocked.
---
## File Structure
| File | Action | Responsibility |
|---|---|---|
| `sshinstall.sh` | Create | Linux SSH bootstrap — install, enable, firewall, deploy key |
| `sshinstall.ps1` | Create | Windows SSH bootstrap — same, via winget-free native/portable OpenSSH |
| `README.md` | Modify | Add `sshinstall.sh`/`sshinstall.ps1` to the usage examples + scripts table |
---
### Task 1: `sshinstall.sh`
**Files:**
- Create: `sshinstall.sh`
**Interfaces:**
- Produces: a standalone bash script, no dependency on any other file in this repo.
Invoked as `curl -s .../sshinstall.sh | sudo bash`.
- [ ] **Step 1: Write the file**
Write to `sshinstall.sh`:
```bash
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# sshinstall.sh — SSH bootstrap (install + enable sshd + deploy admin key)
# Debian/Ubuntu / Arch / RHEL/Fedora
#
# curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.sh | sudo bash
# ═══════════════════════════════════════════════════════════════════
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${CYAN}[•]${NC} $1"; }
success() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
skip() { echo -e "${MAGENTA}[~]${NC} $1 (already done)"; }
die() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
header() { echo -e "\n${BOLD}${CYAN}── $1 ──────────────────────────────${NC}"; }
ADMIN_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop"
echo -e "${BOLD}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ sshinstall.sh — SSH bootstrap ║"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"
# ── Require root ─────────────────────────────────────────────────
[[ "$EUID" -eq 0 ]] || die "Must be run as root — re-run with sudo"
# ── Detect distro ─────────────────────────────────────────────────
header "Detecting system"
[[ -f /etc/os-release ]] && . /etc/os-release || die "Cannot detect distro"
DISTRO_ID="${ID,,}"
DISTRO_LIKE="${ID_LIKE,,}"
info "Distro: ${PRETTY_NAME}"
if command -v pacman &>/dev/null; then
FAMILY="arch"
elif [[ "$DISTRO_ID" =~ ^(kali|parrot|debian|ubuntu|linuxmint|pop)$ ]] || \
[[ "$DISTRO_LIKE" =~ debian ]] || [[ "$DISTRO_LIKE" =~ ubuntu ]]; then
FAMILY="debian"
elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then
FAMILY="fedora"
else
die "Unsupported distro: $DISTRO_ID"
fi
info "Family: $FAMILY"
# ── Install SSH server ────────────────────────────────────────────
header "SSH server package"
case $FAMILY in
arch)
SERVICE="sshd"
if pacman -Qi openssh &>/dev/null; then
skip "openssh"
else
info "Installing openssh..."
pacman -Sy --noconfirm --needed openssh
success "openssh installed"
fi
;;
debian)
SERVICE="ssh"
if dpkg -l openssh-server 2>/dev/null | grep -q "^ii"; then
skip "openssh-server"
else
info "Installing openssh-server..."
apt-get update -qq
apt-get install -y -qq openssh-server
success "openssh-server installed"
fi
;;
fedora)
SERVICE="sshd"
if rpm -q openssh-server &>/dev/null; then
skip "openssh-server"
else
info "Installing openssh-server..."
if command -v dnf &>/dev/null; then
dnf install -y -q openssh-server
else
yum install -y -q openssh-server
fi
success "openssh-server installed"
fi
;;
esac
# ── Enable + start service ────────────────────────────────────────
header "SSH service"
if systemctl is-active --quiet "$SERVICE"; then
skip "$SERVICE service"
else
info "Enabling + starting $SERVICE..."
systemctl enable --now "$SERVICE"
success "$SERVICE running"
fi
# ── Firewall ───────────────────────────────────────────────────────
header "Firewall"
if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then
if ufw status | grep -qE "^22(/tcp)?[[:space:]]+ALLOW"; then
skip "ufw port 22"
else
info "Opening port 22 via ufw..."
ufw allow 22/tcp
success "ufw rule added"
fi
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
if firewall-cmd --list-services | grep -qw ssh; then
skip "firewalld ssh service"
else
info "Opening ssh via firewalld..."
firewall-cmd --add-service=ssh --permanent
firewall-cmd --reload
success "firewalld rule added"
fi
else
warn "No active ufw/firewalld found — skipping firewall step (port 22 may already be open, or another firewall manager is in use)"
fi
# ── Deploy admin key ───────────────────────────────────────────────
header "Admin key"
REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || echo $USER)}"
REAL_HOME=$(eval echo "~$REAL_USER")
SSH_DIR="$REAL_HOME/.ssh"
AUTH_KEYS="$SSH_DIR/authorized_keys"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
touch "$AUTH_KEYS"
chmod 600 "$AUTH_KEYS"
chown -R "$REAL_USER" "$SSH_DIR"
if grep -qF "$ADMIN_KEY" "$AUTH_KEYS" 2>/dev/null; then
skip "admin key in $AUTH_KEYS"
else
echo "$ADMIN_KEY" >> "$AUTH_KEYS"
success "admin key added to $AUTH_KEYS"
fi
echo -e "${BOLD}${GREEN}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ Done — SSH ready, key deployed ║"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"
```
- [ ] **Step 2: Make it executable and lint it**
```bash
chmod +x sshinstall.sh
bash -n sshinstall.sh
```
Expected: `bash -n` (syntax check only) produces no output and exits 0.
- [ ] **Step 3: Manual verification checklist**
Run on at least one real or VM target per distro family available:
1. As a non-root user: `sudo bash sshinstall.sh` — confirm it runs end to end, installs
`openssh-server`/`openssh` if missing, enables+starts the service, opens the firewall
(or reports it's already open), and appends the admin key to
`<real-user-home>/.ssh/authorized_keys` (not `/root/.ssh/`, unless run as actual root
directly).
2. Run it again immediately — confirm every step now prints via `skip` (already
done/installed/running/open/present), and `authorized_keys` has exactly one copy of
the key line (`grep -c` the exact key string, expect `1`).
3. From a machine holding the matching private key, `ssh <target>` — confirm it connects
with no password prompt.
4. Run it as a plain user without `sudo` — confirm it fails immediately with the "Must be
run as root" message and makes no changes.
- [ ] **Step 4: Commit**
```bash
cd /nvme-data/Scripts
git add sshinstall.sh
git commit -m "Add sshinstall.sh — Linux SSH bootstrap + admin key deploy"
```
---
### Task 2: `sshinstall.ps1`
**Files:**
- Create: `sshinstall.ps1`
**Interfaces:**
- Produces: a standalone PowerShell script, no dependency on any other file in this repo
(and no dependency on `winget`, unlike `apps.ps1` — OpenSSH isn't a winget package on
most systems). Invoked as `irm .../sshinstall.ps1 | iex`.
- [ ] **Step 1: Write the file**
Write to `sshinstall.ps1`:
```powershell
# ═══════════════════════════════════════════════════════════════════
# sshinstall.ps1
# Windows equivalent of sshinstall.sh — installs/enables OpenSSH Server,
# deploys the shared admin key for passwordless login
# Run in PowerShell as Administrator:
# irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
# ═══════════════════════════════════════════════════════════════════
#Requires -RunAsAdministrator
$ErrorActionPreference = "Stop"
function Write-Header { param($t) Write-Host "`n── $t ──────────────────────────────" -ForegroundColor Cyan }
function Write-Info { param($t) Write-Host "[•] $t" -ForegroundColor Cyan }
function Write-Success { param($t) Write-Host "[✓] $t" -ForegroundColor Green }
function Write-Skip { param($t) Write-Host "[~] $t (already done)" -ForegroundColor Magenta }
function Write-Warn { param($t) Write-Host "[!] $t" -ForegroundColor Yellow }
$AdminKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop"
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host " ║ sshinstall.ps1 — SSH bootstrap ║" -ForegroundColor Cyan
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# ── Install OpenSSH Server ────────────────────────────────────────
Write-Header "SSH server"
$capability = Get-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" -ErrorAction SilentlyContinue
$sshdPath = "$env:ProgramFiles\OpenSSH\sshd.exe"
if ($capability -and $capability.State -eq "Installed") {
Write-Skip "OpenSSH Server (Windows capability)"
} elseif ($capability) {
Write-Info "Installing OpenSSH Server via Windows capability..."
Add-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" | Out-Null
Write-Success "OpenSSH Server capability installed"
} elseif (Test-Path $sshdPath) {
Write-Skip "OpenSSH Server (portable install found at $sshdPath)"
} else {
Write-Warn "Windows capability not available (likely Windows 8) — falling back to portable Win32-OpenSSH install"
$zipUrl = "https://github.com/PowerShell/Win32-OpenSSH/releases/latest/download/OpenSSH-Win64.zip"
$zipPath = "$env:TEMP\OpenSSH-Win64.zip"
$installDir = "$env:ProgramFiles\OpenSSH"
Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath
Expand-Archive -Path $zipPath -DestinationPath $env:ProgramFiles -Force
if (Test-Path "$env:ProgramFiles\OpenSSH-Win64") {
Move-Item "$env:ProgramFiles\OpenSSH-Win64\*" $installDir -Force
Remove-Item "$env:ProgramFiles\OpenSSH-Win64" -Recurse -Force
}
Push-Location $installDir
& "$installDir\install-sshd.ps1"
Pop-Location
Write-Success "Portable OpenSSH Server installed to $installDir"
}
# ── Enable + start service ────────────────────────────────────────
Write-Header "SSH service"
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq "Running") {
Write-Skip "sshd service"
} else {
Write-Info "Enabling + starting sshd..."
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
Write-Success "sshd running"
}
# ── Firewall ───────────────────────────────────────────────────────
Write-Header "Firewall"
$rule = Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object {
$_.Enabled -eq "True" -and $_.Direction -eq "Inbound" -and
(Get-NetFirewallPortFilter -AssociatedNetFirewallRule $_ -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -eq 22 })
}
if ($rule) {
Write-Skip "Inbound firewall rule for port 22"
} else {
Write-Info "Adding inbound firewall rule for port 22..."
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP" -DisplayName "OpenSSH Server (sshinstall)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
Write-Success "Firewall rule added"
}
# ── Deploy admin key ───────────────────────────────────────────────
Write-Header "Admin key"
$isAdminAccount = ([Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains 'S-1-5-32-544')
if ($isAdminAccount) {
$keyFile = "$env:ProgramData\ssh\administrators_authorized_keys"
} else {
$keyFile = "$env:USERPROFILE\.ssh\authorized_keys"
}
$keyDir = Split-Path $keyFile
if (-not (Test-Path $keyDir)) { New-Item -ItemType Directory -Path $keyDir -Force | Out-Null }
if (-not (Test-Path $keyFile)) { New-Item -ItemType File -Path $keyFile -Force | Out-Null }
$existing = Get-Content $keyFile -ErrorAction SilentlyContinue
if ($existing -contains $AdminKey) {
Write-Skip "admin key in $keyFile"
} else {
Add-Content -Path $keyFile -Value $AdminKey
Write-Success "admin key added to $keyFile"
}
if ($isAdminAccount) {
icacls $keyFile /inheritance:r | Out-Null
icacls $keyFile /grant "SYSTEM:F" | Out-Null
icacls $keyFile /grant "Administrators:F" | Out-Null
Write-Success "Locked down ACLs on $keyFile (SYSTEM + Administrators only)"
}
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ Done — SSH ready, key deployed ║" -ForegroundColor Green
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
```
- [ ] **Step 2: Syntax-check it**
```powershell
pwsh -NoProfile -Command "[System.Management.Automation.PSParser]::Tokenize((Get-Content -Raw sshinstall.ps1), [ref]$null) | Out-Null; Write-Host 'Syntax OK'"
```
If `pwsh` (PowerShell 7+) isn't installed anywhere this can be tested, note that
explicitly rather than skipping the check silently — this is the one file in this plan
that can't be `bash -n`'d, so eyeballing the brace/paren balance carefully during Step 1
matters more than usual.
- [ ] **Step 3: Manual verification checklist**
Run on at least one real or VM Windows 10/11 target, and on a Windows 8 target/VM if one
is available:
1. In a non-elevated PowerShell window, run the script — confirm PowerShell's own
`#Requires -RunAsAdministrator` check refuses to run it (no changes made).
2. As Administrator, run it — confirm the native `Add-WindowsCapability` path is taken
on 10/11 (or the portable-binary fallback is taken and actually completes on 8),
`sshd` ends up `Running`, a port-22 inbound firewall rule exists, and the admin key
lands in `administrators_authorized_keys` if the invoking account is in the local
Administrators group (the common case), or `.ssh\authorized_keys` otherwise.
3. Run `icacls "$env:ProgramData\ssh\administrators_authorized_keys"` — confirm only
`SYSTEM` and `Administrators` have access (no inherited broader permissions).
4. Run the script again — confirm everything reports via `Write-Skip`, and the key file
has exactly one copy of the key line.
5. From a machine holding the matching private key, `ssh <target>` — confirm it connects
with no password prompt.
- [ ] **Step 4: Commit**
```bash
cd /nvme-data/Scripts
git add sshinstall.ps1
git commit -m "Add sshinstall.ps1 — Windows SSH bootstrap + admin key deploy"
```
---
### Task 3: README update + push
**Files:**
- Modify: `README.md`
**Interfaces:** none (documentation only).
- [ ] **Step 1: Add usage examples**
In the "Quick usage" section, after the existing `apps.ps1` line, add:
```markdown
# Bootstrap SSH access on a new machine (installs+enables sshd, deploys admin key)
curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.sh | sudo bash
# Windows (PowerShell as Admin)
irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
```
- [ ] **Step 2: Add a table row**
In the "Scripts" table, add:
```markdown
| `sshinstall.sh` | Install/enable SSH server + deploy admin key | Arch, Debian, Ubuntu, RHEL, Fedora |
| `sshinstall.ps1` | Windows equivalent | Windows 8, 10, 11 |
```
- [ ] **Step 3: Commit**
```bash
cd /nvme-data/Scripts
git add README.md
git commit -m "docs: add sshinstall.sh/.ps1 usage"
```
- [ ] **Step 4: Push, once SSH access to the git server is available**
```bash
cd /nvme-data/Scripts
git push origin master
```
If `[email protected]` is still unreachable (locked SSH key), stop here and tell the user
these 3 commits are local-only and need a push once their key is unlocked — do not
attempt a workaround (switching to HTTPS with embedded credentials, etc.).
@@ -0,0 +1,144 @@
# sshinstall — cross-platform SSH bootstrap — design
**Date:** 2026-09-17
**Repo:** `git.edemilov.ru/edemilov/Scripts` (same repo as `apps.sh`/`apps.ps1`)
**Status:** approved, not yet implemented
## Context
The user has many PCs across work and home and doesn't want to manually set up SSH
access (install server, enable it, open the firewall, copy a key) on each one by hand.
This adds a curl/iwr-pipeable pair — `sshinstall.sh` (Linux) and `sshinstall.ps1`
(Windows) — that installs and enables an SSH server on the target and deploys a known
public key into its `authorized_keys`, so the very first connection afterward is already
passwordless. Follows the exact usage convention already established by `apps.sh`/
`apps.ps1` in this same repo (see `README.md`):
```bash
curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.sh | sudo bash
```
```powershell
irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
```
## Key deployed
One shared "admin" keypair across all of the user's own devices — not a new dedicated
keypair, not per-device keys. The user chose to reuse `Len7Ryzen` (laptop)'s existing
public key as this shared identity, since they already use the same passphrase
everywhere. The **public** half is embedded as a literal string directly in both scripts
(public keys are safe to publish; this also keeps the script fully self-contained — no
second network fetch dependency at install time). The **private** half needs to exist on
every machine the user wants to *connect from* — currently only the laptop has it;
syncing it onto the desktop is tracked separately as a manual reminder (vault
`Reminders/2026-09-17-sshinstall-admin-key-sync.md`), not something this script does.
**The admin public key** (`Len7Ryzen`, supplied 2026-09-17):
```
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop
```
## Behavior (both scripts)
1. **Require elevation, fail fast.** Check for root (Linux: `EUID -ne 0`) / Administrator
(Windows: `[Security.Principal.WindowsIdentity]`). If not elevated, print one clear
line telling the user to re-run with `sudo` / as Administrator, and exit non-zero. No
auto-elevation / re-exec attempt — not worth the complexity for a tool the user runs
deliberately on machines they already control.
2. **Detect the platform.**
- Linux: read `/etc/os-release`'s `ID`/`ID_LIKE` to pick the package manager —
`apt` (Debian/Ubuntu), `pacman` (Arch), `dnf`/`yum` (RHEL/Fedora/CentOS) — matching
the distro-family detection already used in `apps.sh` for this same repo.
- Windows: `[Environment]::OSVersion` / `Get-ComputerInfo` to get the build number.
3. **Install the SSH server, if not already present.**
- Linux: `openssh-server` via the detected package manager (idempotent — package
managers already no-op on an installed package).
- Windows: `Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0` (native
path, Windows 10 1809+ and 11, Windows-Update-maintained binaries).
**Revised 2026-09-17, after final review**: the original design also planned a
portable Win32-OpenSSH-zip fallback for Windows 8 (no `Add-WindowsCapability` at
all). Dropped — it depended on `Expand-Archive` (PowerShell 5.0+) and TLS 1.2
support neither of which exist on stock Windows 8's PowerShell 3.0/4.0, so the one
path that existed specifically for Windows 8 was guaranteed to fail on Windows 8. If
the capability isn't available, the script now just fails with one clear
unsupported-OS message instead of attempting a fallback that can't work. **Windows
support is 10/11 only**, not 8/10/11 as originally scoped.
4. **Enable and start the service, if not already running.**
- Linux: `systemctl enable --now sshd` (or `ssh` — service name varies by distro;
check both).
- Windows: `Set-Service -Name sshd -StartupType Automatic; Start-Service sshd`.
5. **Open the firewall for port 22, only if not already open** (idempotent — check
current rules before adding, matching the "check present state before acting"
convention used for the key-deployment step below).
- Linux: `ufw allow 22/tcp` if `ufw` is active, else `firewall-cmd
--add-service=ssh --permanent && firewall-cmd --reload` if `firewalld` is active,
else leave a note that no known firewall manager was found (don't guess at raw
`iptables` rules).
- Windows: `New-NetFirewallRule` for the OpenSSH Server rule, only if an enabled rule
for port 22 doesn't already exist (the native capability install actually creates
this automatically — the manual/portable fallback path needs to do it explicitly).
6. **Deploy the embedded public key, idempotently.**
- Linux: append to the invoking real user's `~/.ssh/authorized_keys` (the user who
ran `sudo`, resolved via `$SUDO_USER` — not literally `/root/`, unless the script
is run as root directly with no `sudo`), creating
`~/.ssh` (mode `700`) and the file (mode `600`) if missing. Skip the append if the
exact key line is already present (idempotent re-run).
- Windows: **must** go into `%ProgramData%\ssh\administrators_authorized_keys`, not
the per-user `.ssh\authorized_keys` — this is a well-known Windows OpenSSH quirk:
for any account in the local Administrators group, `sshd` only reads this one
central file, and it silently falls back to password auth if the file has the
wrong ACLs. After writing the key, run `icacls` to set the file to
Administrators/SYSTEM-only (matching Microsoft's documented requirement), or key
auth will fail even though the file's contents are correct.
7. **Password auth stays enabled.** No `PasswordAuthentication no` / `sshd_config`
hardening in this script — this is a personal convenience tool for machines the user
already controls, not a hardened bastion. A `--harden` flag to disable password auth
after confirming key-based login works could be a future addition; not built now.
8. **Print a short summary at the end**: which steps were already satisfied vs. newly
done (server installed? already there? key added? already present? firewall opened?
already open?), so a re-run's output is clearly different from a fresh run's, and it's
obvious at a glance that everything succeeded. No live test-connection attempt from
the target back to anywhere — the user just tries connecting from their own machine
afterward.
## Idempotency
Every step checks current state before acting (package already installed, service
already running, firewall rule already present, key line already present) so running the
script twice on the same machine is always safe and just reports "already done" for
anything that doesn't need redoing.
## Out of scope / explicitly deferred
- Auto-disabling password authentication after key deployment (future `--harden` flag).
- Self-elevation / automatic re-exec as admin/root.
- Generating a brand-new dedicated keypair — reuses the laptop's existing one.
- Per-device separate keys.
- Syncing the private key itself onto other machines — that's a manual step tracked in a
vault reminder, not something either script does.
- An uninstall/removal path (not discussed; flag for a later round if wanted).
- Any test-connection round-trip from inside the script.
## Testing / verification plan
No CI/automated test harness for this repo (matches `apps.sh`'s existing testing
approach — real-machine verification only). Plan:
1. Run `sshinstall.sh` on at least one real target per Linux family (a Debian/Ubuntu box,
an Arch box, a RHEL/Fedora box if one is available) — confirm server installs,
service starts, firewall opens (or is confirmed already open), key line appears
exactly once in `~/.ssh/authorized_keys`.
2. Re-run it immediately after on the same box — confirm the summary reports everything
as "already done" and the key line is not duplicated.
3. Run `sshinstall.ps1` on a Windows 11 (or 10) box — confirm the native
`Add-WindowsCapability` path is taken, the key lands in
`administrators_authorized_keys` with correct ACLs (verify via `icacls`), and
`ssh <target>` from a machine holding the laptop's private key connects with no
password prompt.
4. On a machine without the OpenSSH capability available (or an older Windows build),
confirm the script fails with the clear unsupported-OS message rather than
attempting an install.
5. Confirm a non-elevated run on both platforms fails fast with the one-line error and
makes no changes.
+131
View File
@@ -0,0 +1,131 @@
# ═══════════════════════════════════════════════════════════════════
# sshinstall.ps1
# Windows equivalent of sshinstall.sh — installs/enables OpenSSH Server,
# deploys the shared admin key for passwordless login
# Run in PowerShell as Administrator:
# irm https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.ps1 | iex
# ═══════════════════════════════════════════════════════════════════
#Requires -RunAsAdministrator
$ErrorActionPreference = "Stop"
function Write-Header { param($t) Write-Host "`n── $t ──────────────────────────────" -ForegroundColor Cyan }
function Write-Info { param($t) Write-Host "[•] $t" -ForegroundColor Cyan }
function Write-Success { param($t) Write-Host "[✓] $t" -ForegroundColor Green }
function Write-Skip { param($t) Write-Host "[~] $t (already done)" -ForegroundColor Magenta }
function Write-Warn { param($t) Write-Host "[!] $t" -ForegroundColor Yellow }
# ── Require elevation ────────────────────────────────────────────
# #Requires -RunAsAdministrator above does not enforce under
# `irm ... | iex` (piping into Invoke-Expression is widely reported not to
# trigger the #Requires check), so this explicit check is the real guard.
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Warn "Must be run as Administrator — re-run PowerShell as Administrator"
exit 1
}
$AdminKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop"
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Cyan
Write-Host " ║ sshinstall.ps1 — SSH bootstrap ║" -ForegroundColor Cyan
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Cyan
Write-Host ""
# ── Install OpenSSH Server ────────────────────────────────────────
Write-Header "SSH server"
$capability = Get-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" -ErrorAction SilentlyContinue
$sshdPath = "$env:ProgramFiles\OpenSSH\sshd.exe"
if ($capability -and $capability.State -eq "Installed") {
Write-Skip "OpenSSH Server (Windows capability)"
} elseif ($capability) {
Write-Info "Installing OpenSSH Server via Windows capability..."
Add-WindowsCapability -Online -Name "OpenSSH.Server~~~~0.0.1.0" | Out-Null
Write-Success "OpenSSH Server capability installed"
} elseif (Test-Path $sshdPath) {
Write-Skip "OpenSSH Server (portable install found at $sshdPath)"
} else {
Write-Warn "OpenSSH Server capability is not available on this Windows version. This script supports Windows 10 (1809+) and Windows 11 only."
exit 1
}
# ── Enable + start service ────────────────────────────────────────
Write-Header "SSH service"
$svc = Get-Service -Name sshd -ErrorAction SilentlyContinue
if ($svc -and $svc.Status -eq "Running") {
Write-Skip "sshd service"
} else {
Write-Info "Enabling + starting sshd..."
Set-Service -Name sshd -StartupType Automatic
Start-Service sshd
Write-Success "sshd running"
}
# ── Firewall ───────────────────────────────────────────────────────
Write-Header "Firewall"
$rule = Get-NetFirewallRule -ErrorAction SilentlyContinue | Where-Object {
$_.Enabled -eq "True" -and $_.Direction -eq "Inbound" -and
(Get-NetFirewallPortFilter -AssociatedNetFirewallRule $_ -ErrorAction SilentlyContinue | Where-Object { $_.LocalPort -eq 22 })
}
if ($rule) {
Write-Skip "Inbound firewall rule for port 22"
} else {
Write-Info "Adding inbound firewall rule for port 22..."
New-NetFirewallRule -Name "OpenSSH-Server-In-TCP-sshinstall" -DisplayName "OpenSSH Server (sshinstall)" -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 | Out-Null
Write-Success "Firewall rule added"
}
# ── Deploy admin key ───────────────────────────────────────────────
Write-Header "Admin key"
$adminSid = New-Object System.Security.Principal.SecurityIdentifier("S-1-5-32-544")
$isAdminAccount = ([Security.Principal.WindowsIdentity]::GetCurrent().Groups -contains $adminSid)
if ($isAdminAccount) {
$keyFile = "$env:ProgramData\ssh\administrators_authorized_keys"
} else {
$keyFile = "$env:USERPROFILE\.ssh\authorized_keys"
}
$keyDir = Split-Path $keyFile
if (-not (Test-Path $keyDir)) { New-Item -ItemType Directory -Path $keyDir -Force | Out-Null }
if (-not (Test-Path $keyFile)) { New-Item -ItemType File -Path $keyFile -Force | Out-Null }
# Add-Content resumes writing at the file's current end-of-file position and only
# guarantees a trailing newline after what IT writes — it does not fix up a pre-existing
# last line that lacks one (e.g. a hand-edited or externally-written file). Without this
# guard that can glue the new key onto the end of the last existing line, corrupting both,
# same as the bash >> case this fix wave addresses.
if ((Get-Item $keyFile).Length -gt 0) {
$existingRaw = Get-Content -Path $keyFile -Raw
if ($existingRaw.Length -gt 0 -and $existingRaw[-1] -ne "`n") {
Add-Content -Path $keyFile -Value ""
}
}
$existing = Get-Content $keyFile -ErrorAction SilentlyContinue
if ($existing -contains $AdminKey) {
Write-Skip "admin key in $keyFile"
} else {
Add-Content -Path $keyFile -Value $AdminKey
Write-Success "admin key added to $keyFile"
}
if ($isAdminAccount) {
$aclOutput = (icacls $keyFile) -join "`n"
if ($aclOutput -match 'BUILTIN\\Users|Everyone|Authenticated Users|BUILTIN\\Guests') {
icacls $keyFile /inheritance:r | Out-Null
icacls $keyFile /grant "SYSTEM:F" | Out-Null
icacls $keyFile /grant "Administrators:F" | Out-Null
Write-Success "Locked down ACLs on $keyFile (SYSTEM + Administrators only)"
} else {
Write-Skip "ACLs on $keyFile"
}
}
Write-Host ""
Write-Host " ╔══════════════════════════════════════════════╗" -ForegroundColor Green
Write-Host " ║ Done — SSH ready, key deployed ║" -ForegroundColor Green
Write-Host " ╚══════════════════════════════════════════════╝" -ForegroundColor Green
Write-Host ""
Executable
+150
View File
@@ -0,0 +1,150 @@
#!/bin/bash
# ═══════════════════════════════════════════════════════════════════
# sshinstall.sh — SSH bootstrap (install + enable sshd + deploy admin key)
# Debian/Ubuntu / Arch / RHEL/Fedora
#
# curl -s https://git.edemilov.ru/edemilov/scripts/raw/branch/master/sshinstall.sh | sudo bash
# ═══════════════════════════════════════════════════════════════════
set -e
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; MAGENTA='\033[0;35m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${CYAN}[•]${NC} $1"; }
success() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
skip() { echo -e "${MAGENTA}[~]${NC} $1 (already done)"; }
die() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
header() { echo -e "\n${BOLD}${CYAN}── $1 ──────────────────────────────${NC}"; }
ADMIN_KEY="ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH/hWD8YgjVzA/9V364W4w2HiEpuPjKMQ/Id3SRlyriM edemilov@laptop"
echo -e "${BOLD}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ sshinstall.sh — SSH bootstrap ║"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"
# ── Require root ─────────────────────────────────────────────────
[[ "$EUID" -eq 0 ]] || die "Must be run as root — re-run with sudo"
# ── Detect distro ─────────────────────────────────────────────────
header "Detecting system"
[[ -f /etc/os-release ]] && . /etc/os-release || die "Cannot detect distro"
DISTRO_ID="${ID,,}"
DISTRO_LIKE="${ID_LIKE,,}"
info "Distro: ${PRETTY_NAME}"
if command -v pacman &>/dev/null; then
FAMILY="arch"
elif [[ "$DISTRO_ID" =~ ^(kali|parrot|debian|ubuntu|linuxmint|pop)$ ]] || \
[[ "$DISTRO_LIKE" =~ debian ]] || [[ "$DISTRO_LIKE" =~ ubuntu ]]; then
FAMILY="debian"
elif command -v dnf &>/dev/null || command -v yum &>/dev/null; then
FAMILY="fedora"
else
die "Unsupported distro: $DISTRO_ID"
fi
info "Family: $FAMILY"
# ── Install SSH server ────────────────────────────────────────────
header "SSH server package"
case $FAMILY in
arch)
SERVICE="sshd"
if pacman -Qi openssh &>/dev/null; then
skip "openssh"
else
info "Installing openssh..."
pacman -S --noconfirm --needed openssh
success "openssh installed"
fi
;;
debian)
SERVICE="ssh"
if dpkg -l openssh-server 2>/dev/null | grep -q "^ii"; then
skip "openssh-server"
else
info "Installing openssh-server..."
apt-get update -qq
apt-get install -y -qq openssh-server
success "openssh-server installed"
fi
;;
fedora)
SERVICE="sshd"
if rpm -q openssh-server &>/dev/null; then
skip "openssh-server"
else
info "Installing openssh-server..."
if command -v dnf &>/dev/null; then
dnf install -y -q openssh-server
else
yum install -y -q openssh-server
fi
success "openssh-server installed"
fi
;;
esac
# ── Enable + start service ────────────────────────────────────────
header "SSH service"
if systemctl is-active --quiet "$SERVICE"; then
skip "$SERVICE service"
else
info "Enabling + starting $SERVICE..."
systemctl enable --now "$SERVICE"
success "$SERVICE running"
fi
# ── Firewall ───────────────────────────────────────────────────────
header "Firewall"
if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then
if ufw status | grep -qE "^22(/tcp)?[[:space:]]+ALLOW"; then
skip "ufw port 22"
else
info "Opening port 22 via ufw..."
ufw allow 22/tcp
success "ufw rule added"
fi
elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld; then
if firewall-cmd --list-services | grep -qw ssh; then
skip "firewalld ssh service"
else
info "Opening ssh via firewalld..."
firewall-cmd --add-service=ssh --permanent
firewall-cmd --reload
success "firewalld rule added"
fi
else
warn "No active ufw/firewalld found — skipping firewall step (port 22 may already be open, or another firewall manager is in use)"
fi
# ── Deploy admin key ───────────────────────────────────────────────
header "Admin key"
REAL_USER="${SUDO_USER:-$(logname 2>/dev/null || echo $USER)}"
REAL_HOME=$(eval echo "~$REAL_USER")
SSH_DIR="$REAL_HOME/.ssh"
AUTH_KEYS="$SSH_DIR/authorized_keys"
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
touch "$AUTH_KEYS"
chmod 600 "$AUTH_KEYS"
chown -R "$REAL_USER:$REAL_USER" "$SSH_DIR"
if [[ -s "$AUTH_KEYS" ]] && [[ -n "$(tail -c1 "$AUTH_KEYS")" ]]; then
echo >> "$AUTH_KEYS"
fi
if grep -qF "$ADMIN_KEY" "$AUTH_KEYS" 2>/dev/null; then
skip "admin key in $AUTH_KEYS"
else
echo "$ADMIN_KEY" >> "$AUTH_KEYS"
success "admin key added to $AUTH_KEYS"
fi
echo -e "${BOLD}${GREEN}"
echo " ╔══════════════════════════════════════════════╗"
echo " ║ Done — SSH ready, key deployed ║"
echo " ╚══════════════════════════════════════════════╝"
echo -e "${NC}"