Compare commits
10
Commits
d6018b13f0
...
0480c7aed5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0480c7aed5 | ||
|
|
337a209713 | ||
|
|
93d9ead905 | ||
|
|
c4efd37831 | ||
|
|
def3d57799 | ||
|
|
902e95a79d | ||
|
|
088d692954 | ||
|
|
25800331fe | ||
|
|
2a2ed5711b | ||
|
|
9a68a19dea |
No files matched your search
@@ -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
@@ -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
@@ -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}"
|
||||
Reference in new issue
Block a user