forked from finn/tinyboard
Compare commits
44 Commits
96c737709c
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee67739f7 | ||
|
|
39f8f64351 | ||
|
|
e924579b2e | ||
|
|
912e553e06 | ||
|
|
98986e615b | ||
|
|
0e792be751 | ||
|
|
835793d396 | ||
|
|
11f9586c5e | ||
|
|
3e351f925d | ||
|
|
a197b7881b | ||
|
|
60feeca65e | ||
|
|
88fabcf25f | ||
|
|
51f661766f | ||
|
|
5326823b81 | ||
|
|
0f76283605 | ||
|
|
a02a83cae4 | ||
|
|
4a1983d46d | ||
|
|
395ab4ed0e | ||
|
|
4c08f3b389 | ||
|
|
ccd324dc79 | ||
|
|
664bdeaed4 | ||
|
|
ae49c58b13 | ||
|
|
119b747dda | ||
|
|
ea72b14696 | ||
|
|
26110ce8d3 | ||
|
|
58f6445c72 | ||
|
|
08799f0f7f | ||
|
|
a79b1c59b8 | ||
|
|
7e64156026 | ||
|
|
3d366cd74a | ||
|
|
d080db1db8 | ||
|
|
37e3e91239 | ||
|
|
7676a907ee | ||
|
|
e5ecdca3ff | ||
|
|
50fb313f9a | ||
|
|
d21997af43 | ||
|
|
95a56ef4f0 | ||
|
|
b706dd211d | ||
|
|
f3a3f66982 | ||
|
|
384cf476ff | ||
|
|
b8d2a3e5bc | ||
|
|
a49f830ed2 | ||
|
|
fe7f77171f | ||
|
|
288aa698d0 |
308
README.md
308
README.md
@@ -1,208 +1,194 @@
|
||||
# TinyBoard Hub-Spoke File Sharing System
|
||||
# TinyBoard
|
||||
|
||||
A hub-spoke architecture for secure file sharing over SSH tunnels using autossh and rclone.
|
||||
|
||||
## Architecture Overview
|
||||
Spokes are ARM devices (e.g. OrangePi, Raspberry Pi) running Armbian that establish reverse SSH tunnels to a central hub server. The hub mounts spoke filesystems via SFTP using rclone, making files accessible across all devices without exposing them to the internet.
|
||||
|
||||
This system implements a hub-and-spoke model where:
|
||||
- **Spokes**: Raspberry Pi devices running Armbian that establish reverse SSH tunnels to the hub
|
||||
- **Hub**: Central server that mounts spoke filesystems via SFTP using rclone
|
||||
---
|
||||
|
||||
### Key Components
|
||||
## Quickstart
|
||||
|
||||
1. **Spoke Side** (`spoke/` directory):
|
||||
- Docker-based autossh tunnel container
|
||||
- Configuration files for spoke setup
|
||||
- Hostname assignment based on MAC address
|
||||
### Setting up a new Hub
|
||||
|
||||
2. **Hub Side** (`hub/` directory):
|
||||
- Rclone SFTP mount configuration
|
||||
- Systemd user service for automatic mounting
|
||||
On a fresh Debian/Ubuntu VPS or server:
|
||||
|
||||
3. **Management Script** (`hubspoke-helper.sh`):
|
||||
- Unified interface for managing both hub and spoke components
|
||||
```bash
|
||||
apt install git
|
||||
git clone https://gut.oily.dad/justin/tinyboard
|
||||
cd tinyboard
|
||||
./setup.sh # option 4 (setup new hub)
|
||||
```
|
||||
|
||||
### Setting up a new Spoke
|
||||
|
||||
On a fresh Armbian device:
|
||||
|
||||
1. Modify `spoke/armbian.not_logged_in_yet` accordingly, then drop it onto the SD card as `/root/.not_logged_in_yet` before first boot (WiFi credentials) — see [Armbian Autoconfig docs](https://docs.armbian.com/User-Guide_Autoconfig/)
|
||||
2. Boot, SSH in as root
|
||||
3. Run:
|
||||
|
||||
```bash
|
||||
apt install git
|
||||
git clone https://gut.oily.dad/justin/tinyboard
|
||||
cd tinyboard
|
||||
./setup.sh # option 0 (configure network)
|
||||
./setup.sh # option 1 (configure new spoke)
|
||||
```
|
||||
|
||||
### Onboarding a Spoke from the Hub
|
||||
|
||||
Once the spoke tunnel is up, run on the hub:
|
||||
|
||||
```bash
|
||||
cd tinyboard
|
||||
./setup.sh # option 2 (onboard spoke)
|
||||
```
|
||||
|
||||
### Offboarding a Spoke from the Hub
|
||||
|
||||
```bash
|
||||
cd tinyboard
|
||||
./setup.sh # option 3 (offboard spoke)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
[ Spoke ] [ Hub ]
|
||||
OrangePi / RPi VPS / Server
|
||||
Armbian Any Linux
|
||||
|
||||
autossh container ──────────► sshd (GatewayPorts)
|
||||
reverse tunnel port 111xx
|
||||
|
||||
rclone SFTP mount
|
||||
~/mnt/<spoke-name>/
|
||||
```
|
||||
|
||||
Spokes initiate outbound SSH connections to the hub, creating reverse tunnels. The hub then uses rclone to mount each spoke's filesystem over SFTP through the tunnel. No inbound ports need to be open on the spoke.
|
||||
|
||||
---
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
tinyboard/
|
||||
├── hubspoke-helper.sh # Main management script
|
||||
├── hub/
|
||||
│ └── rclone.conf # Rclone SFTP configuration
|
||||
├── setup.sh ← entry point
|
||||
├── spoke/
|
||||
│ ├── compose.yaml # Docker Compose for autossh tunnel
|
||||
│ ├── Dockerfile # autossh container image
|
||||
│ ├── autohostname.sh # Hostname assignment by MAC address
|
||||
│ ├── aptprimary.sh # Initial package installation
|
||||
│ ├── clean_sensitive.sh # Clean WiFi/password from configs
|
||||
│ └── armb-not_logged_in_yet # Armbian first-boot configuration
|
||||
└── README.md # This file
|
||||
│ ├── setup-network.sh ← configure static IP before setup
|
||||
│ ├── setup-spoke.sh ← automated spoke setup
|
||||
│ ├── compose.yaml ← Docker Compose for autossh + syncthing
|
||||
│ ├── Dockerfile ← autossh container
|
||||
│ └── armbian.not_logged_in_yet ← Armbian first-boot WiFi config template
|
||||
└── hub/
|
||||
├── setup-hub.sh ← automated hub setup
|
||||
├── onboard-spoke.sh ← add a new spoke to the hub
|
||||
└── offboard-spoke.sh ← remove a spoke from the hub
|
||||
```
|
||||
|
||||
## Key File Handling (Manual Setup)
|
||||
---
|
||||
|
||||
**IMPORTANT**: The following files must be manually created/configured as they contain sensitive information:
|
||||
## Setup Scripts
|
||||
|
||||
### SSH Keys
|
||||
- `~/.ssh/oilykey2026` on spokes (referenced in `spoke/compose.yaml`)
|
||||
- `~/.ssh/armbian-brie-202604` on hub (referenced in `hub/rclone.conf`)
|
||||
- These keys must be manually generated and distributed
|
||||
### `setup.sh`
|
||||
Entry point. Presents a menu:
|
||||
0. Reconfigure network (static IP via netplan — SSH session will drop, reconnect)
|
||||
1. Set up this device as a new spoke
|
||||
2. Onboard a new spoke from the hub
|
||||
3. Offboard a spoke from the hub
|
||||
4. Set up this device as a new hub
|
||||
|
||||
### Rclone Configuration
|
||||
- `~/.config/rclone/rclone.conf` on hub must be manually created
|
||||
- Use `hub/rclone.conf` as a template
|
||||
- Update host, port, and key_file paths as needed
|
||||
- Manually create rclone mount and permission it (`/mnt/hub` for example)
|
||||
### `spoke/setup-network.sh`
|
||||
Run as root on a new spoke before `setup.sh`. Configures a static IP via netplan. Supports both WiFi and wired interfaces. Automatically reverts if network connectivity is lost after applying the new config.
|
||||
|
||||
### Systemd Service Files
|
||||
- `~/.config/systemd/user/rclone-mount@.service` must be manually copied from `hub/rclone-mount@.service`
|
||||
### `spoke/setup-spoke.sh`
|
||||
Run as root on a new spoke. Handles:
|
||||
- Package installation (apt/dnf/yum/pacman)
|
||||
- Docker installation
|
||||
- SSH server setup
|
||||
- Hostname configuration
|
||||
- SSH key generation and hub authorization
|
||||
- Tunnel port auto-detection on the hub
|
||||
- Docker image build and container start
|
||||
- Optional password auth disable
|
||||
|
||||
## Spoke Setup (Raspberry Pi / Armbian)
|
||||
### `hub/setup-hub.sh`
|
||||
Run as root on a new hub server. Handles:
|
||||
- Package installation (apt/dnf/yum/pacman)
|
||||
- rclone installation
|
||||
- Hub user creation
|
||||
- SSH server configuration (GatewayPorts, AllowTcpForwarding)
|
||||
- FUSE configuration
|
||||
- rclone config directory setup
|
||||
- Optional password auth disable
|
||||
|
||||
### Initial Setup
|
||||
1. Write Armbian minimal image to SD card
|
||||
2. Copy `spoke/armb-not_logged_in_yet` to SD card root `/root/.not_logged_in_yet` (contains WiFi credentials)
|
||||
3. Boot device, SSH in as root with password "1234"
|
||||
4. After first login and setup tasks, `.not_logged_in_yet` will be processed for root and armbian user credentials
|
||||
5. Clone this repository: `git clone <repo-url>`
|
||||
6. Run `spoke/aptprimary.sh` to install required packages
|
||||
7. Run `spoke/autohostname.sh` to assign hostname based on MAC address
|
||||
8. Reboot and test as armbian user
|
||||
### `hub/onboard-spoke.sh`
|
||||
Run as the hub user after a spoke connects. Handles:
|
||||
- SSH key generation and deployment to spoke
|
||||
- rclone remote configuration
|
||||
- Spoke registration in `~/.config/tinyboard/spokes`
|
||||
- Per-spoke crontab entry for auto-mount on reboot
|
||||
|
||||
### SSH Key Setup
|
||||
1. Generate SSH key pair on hub: `ssh-keygen -t ed25519 -f ~/.ssh/armbian-brie-202604`
|
||||
2. Copy public key to spoke: `ssh-copy-id -i ~/.ssh/armbian-brie-202604.pub armbian@<spoke-ip>`
|
||||
3. Generate spoke key: `ssh-keygen -t ed25519 -f ~/.ssh/oilykey2026`
|
||||
4. Copy public key to hub for reverse tunnel authentication
|
||||
### `hub/offboard-spoke.sh`
|
||||
Run as the hub user to remove a spoke. Handles:
|
||||
- Unmounting the spoke filesystem
|
||||
- Removing the crontab entry
|
||||
- Removing the rclone remote
|
||||
- Optionally removing the hub SSH key
|
||||
- Removing from the spoke registry
|
||||
|
||||
### Docker Tunnel Setup
|
||||
```bash
|
||||
# Build the autossh container
|
||||
./hubspoke-helper.sh spoke build
|
||||
---
|
||||
|
||||
# Start the tunnel
|
||||
./hubspoke-helper.sh spoke start
|
||||
## Spoke Registry
|
||||
|
||||
# Check status
|
||||
./hubspoke-helper.sh spoke status
|
||||
The hub maintains a registry of connected spokes at `~/.config/tinyboard/spokes`:
|
||||
|
||||
# View logs
|
||||
./hubspoke-helper.sh spoke logs
|
||||
```
|
||||
rocky 11113 /home/armbian/.ssh/armbian-rocky-202504 /home/armbian/mnt/rocky
|
||||
gouda 11114 /home/armbian/.ssh/armbian-gouda-202504 /home/armbian/mnt/gouda
|
||||
```
|
||||
|
||||
## Hub Setup (Central Server)
|
||||
Each spoke gets its own mount point at `~/mnt/<spoke-name>/` and a dedicated rclone crontab entry.
|
||||
|
||||
### Rclone Configuration
|
||||
1. Install rclone: `apt install rclone fuse`
|
||||
2. Create config directory: `mkdir -p ~/.config/rclone`
|
||||
3. Copy and customize `hub/rclone.conf` to `~/.config/rclone/rclone.conf`
|
||||
4. Update key_file path to point to your SSH private key
|
||||
---
|
||||
|
||||
### FUSE Configuration
|
||||
```bash
|
||||
# Allow other users to access mounts (if needed)
|
||||
sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf
|
||||
## Security
|
||||
|
||||
# Add user to fuse group
|
||||
sudo groupadd fuse
|
||||
sudo usermod -aG fuse $USER
|
||||
```
|
||||
- All communication is over SSH tunnels — no spoke ports exposed to the internet
|
||||
- SSH keys are used for all authentication
|
||||
- Scripts check and auto-fix unsafe file permissions (600/400)
|
||||
- Password authentication can be disabled during setup
|
||||
- Scripts refuse to disable password auth if no authorized keys are present (lockout prevention)
|
||||
- Netplan changes are verified with a 30-second connectivity check before being made permanent
|
||||
|
||||
## Usage
|
||||
---
|
||||
|
||||
### Managing Spoke Tunnels
|
||||
- Docker on spoke should handle autostart of spoke tunnel
|
||||
- Syncthing can be combined in this image
|
||||
- Rename syncthing image and host names per-device in the compose file.
|
||||
## Sensitive Files
|
||||
|
||||
```bash
|
||||
# Build autossh container
|
||||
./hubspoke-helper.sh spoke build
|
||||
Before committing, ensure the following do not contain real credentials:
|
||||
|
||||
# Start/stop/restart tunnel
|
||||
./hubspoke-helper.sh spoke start
|
||||
./hubspoke-helper.sh spoke stop
|
||||
./hubspoke-helper.sh spoke restart
|
||||
- `spoke/armbian.not_logged_in_yet` — contains WiFi SSID, password, and user passwords
|
||||
|
||||
# Check status and logs
|
||||
./hubspoke-helper.sh spoke status
|
||||
./hubspoke-helper.sh spoke logs
|
||||
|
||||
# Show manual autossh command
|
||||
./hubspoke-helper.sh spoke show-cmd
|
||||
```
|
||||
|
||||
### Managing Hub Mounts
|
||||
|
||||
#### Crontab entry:
|
||||
```
|
||||
@reboot /home/armbian/tinyboard/hubspoke-helper.sh hub start-background
|
||||
```
|
||||
|
||||
#### Deprecated: systemd
|
||||
```bash
|
||||
# Install systemd service (after manual file placement)
|
||||
./hubspoke-helper.sh hub install
|
||||
|
||||
# Start/stop rclone mount
|
||||
./hubspoke-helper.sh hub start
|
||||
./hubspoke-helper.sh hub stop
|
||||
|
||||
# Check service status
|
||||
./hubspoke-helper.sh hub status
|
||||
|
||||
# Manual mount/unmount for testing
|
||||
./hubspoke-helper.sh hub mount
|
||||
./hubspoke-helper.sh hub unmount
|
||||
```
|
||||
|
||||
## Configuration Variables
|
||||
|
||||
Environment variables can override defaults:
|
||||
- `TUNNEL_DIR`: Directory containing spoke Docker files (default: `~/tinyboard/spoke`)
|
||||
- `COMPOSE_FILE`: Docker compose file path (default: `$TUNNEL_DIR/compose.yaml`)
|
||||
- `RCLONE_REMOTE`: Rclone remote name (default: `brie-remote`)
|
||||
- `MOUNT_POINT`: Mount point on hub (default: `~/mnt/hub`)
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **SSH Keys**: Always use strong key pairs and protect private keys
|
||||
2. **Configuration Files**: Use `spoke/clean_sensitive.sh` to remove WiFi credentials before committing
|
||||
3. **Firewall**: Ensure proper firewall rules on hub (port 11111 for reverse tunnels)
|
||||
4. **User Permissions**: Run services with minimal required privileges
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Spoke Tunnel Issues
|
||||
- Check Docker container logs: `./hubspoke-helper.sh spoke logs`
|
||||
- Verify SSH key permissions: `chmod 600 ~/.ssh/oilykey2026`
|
||||
- Test SSH connection manually: `ssh -p 11111 armbian@localhost`
|
||||
### `apt update` fails with beta.armbian.com error
|
||||
|
||||
### Hub Mount Issues
|
||||
- Check service status: `./hubspoke-helper.sh hub status`
|
||||
- Test rclone manually: `rclone lsd brie-sftp:`
|
||||
- Verify fuse configuration: `ls -la /etc/fuse.conf`
|
||||
- Check user groups: `groups $USER`
|
||||
On some Armbian images, a beta apt repository is enabled by default and may cause `apt update` to fail. Comment it out:
|
||||
|
||||
### Network Issues
|
||||
- Ensure spokes can reach hub on SSH port (22)
|
||||
- Verify reverse tunnel port (11111) is not blocked by firewall
|
||||
- Check DNS resolution on spokes for hub hostname
|
||||
```bash
|
||||
grep -r "beta.armbian" /etc/apt/sources.list /etc/apt/sources.list.d/
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
Open the file that contains it (usually `/etc/apt/sources.list.d/armbian.sources`) and comment out or remove the line referencing `beta.armbian.com`, then run `apt update` again.
|
||||
|
||||
### Updating Configuration
|
||||
1. Update `spoke/compose.yaml` for new spoke hostnames
|
||||
2. Update `hub/rclone.conf` for new spoke connections
|
||||
3. Update `spoke/autohostname.sh` for new MAC addresses
|
||||
---
|
||||
|
||||
### Adding New Spokes
|
||||
1. Follow Spoke Setup steps for new device
|
||||
2. Add MAC address to `spoke/autohostname.sh`
|
||||
3. Update hub's SSH authorized_keys with new spoke public key
|
||||
4. Add new rclone remote configuration if needed
|
||||
## Requirements
|
||||
|
||||
## License
|
||||
|
||||
This project is for personal use. Adapt as needed for your environment.
|
||||
**Spoke:** Armbian (Debian-based), ARM device, Docker, autossh, git
|
||||
|
||||
**Hub:** Any Linux server (Debian/Ubuntu/RHEL/Arch), rclone, fuse, openssh-server
|
||||
|
||||
129
hub/offboard-spoke.sh
Executable file
129
hub/offboard-spoke.sh
Executable file
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RCLONE_CONF="${HOME}/.config/rclone/rclone.conf"
|
||||
SSH_DIR="${HOME}/.ssh"
|
||||
REGISTRY="${HOME}/.config/tinyboard/spokes"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
die "Missing required dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
die "Run as the hub user, not root."
|
||||
fi
|
||||
|
||||
check_deps rclone crontab fusermount python3
|
||||
|
||||
header "TinyBoard Hub — Offboard Spoke"
|
||||
|
||||
[ -f "$REGISTRY" ] || die "No spoke registry found at $REGISTRY. No spokes to offboard."
|
||||
|
||||
echo "Registered spokes:"
|
||||
echo ""
|
||||
awk '{print " " $1 " (port " $2 ", mount " $4 ")"}' "$REGISTRY"
|
||||
echo ""
|
||||
|
||||
read -rp "Spoke name to offboard: " SPOKE_NAME
|
||||
[ -n "$SPOKE_NAME" ] || die "Spoke name cannot be empty"
|
||||
|
||||
SPOKE_LINE=$(grep "^$SPOKE_NAME " "$REGISTRY" 2>/dev/null || true)
|
||||
[ -n "$SPOKE_LINE" ] || die "Spoke '$SPOKE_NAME' not found in registry."
|
||||
|
||||
TUNNEL_PORT=$(echo "$SPOKE_LINE" | awk '{print $2}')
|
||||
KEY_PATH=$(echo "$SPOKE_LINE" | awk '{print $3}')
|
||||
MOUNT_POINT=$(echo "$SPOKE_LINE" | awk '{print $4}')
|
||||
|
||||
echo ""
|
||||
echo -e " Spoke: ${YELLOW}$SPOKE_NAME${NC}"
|
||||
echo -e " Port: ${YELLOW}$TUNNEL_PORT${NC}"
|
||||
echo -e " Key: ${YELLOW}$KEY_PATH${NC}"
|
||||
echo -e " Mount: ${YELLOW}$MOUNT_POINT${NC}"
|
||||
echo ""
|
||||
read -rp "Are you sure you want to offboard $SPOKE_NAME? [y/N]: " CONFIRM
|
||||
[[ "${CONFIRM,,}" == "y" ]] || die "Aborted."
|
||||
|
||||
header "Unmounting Spoke"
|
||||
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
|
||||
if fusermount -u "$MOUNT_POINT" 2>/dev/null; then
|
||||
info "Unmounted $MOUNT_POINT."
|
||||
else
|
||||
warn "Could not unmount $MOUNT_POINT — may already be unmounted."
|
||||
fi
|
||||
else
|
||||
warn "$MOUNT_POINT is not currently mounted."
|
||||
fi
|
||||
|
||||
header "Removing Crontab Entry"
|
||||
EXISTING=$(crontab -l 2>/dev/null || true)
|
||||
UPDATED=$(echo "$EXISTING" | grep -v "${SPOKE_NAME}-remote:" || true)
|
||||
if [ "$EXISTING" = "$UPDATED" ]; then
|
||||
warn "No crontab entry found for $SPOKE_NAME."
|
||||
elif [ -z "$UPDATED" ]; then
|
||||
crontab -r 2>/dev/null || true
|
||||
info "Crontab entry for $SPOKE_NAME removed (crontab now empty)."
|
||||
else
|
||||
echo "$UPDATED" | crontab -
|
||||
info "Crontab entry for $SPOKE_NAME removed."
|
||||
fi
|
||||
|
||||
header "Removing rclone Remote"
|
||||
if grep -q "\[${SPOKE_NAME}-remote\]" "$RCLONE_CONF" 2>/dev/null; then
|
||||
python3 - "$RCLONE_CONF" "$SPOKE_NAME" <<'PYEOF'
|
||||
import sys
|
||||
path, name = sys.argv[1], sys.argv[2]
|
||||
lines = open(path).readlines()
|
||||
out, skip = [], False
|
||||
for line in lines:
|
||||
if line.strip() == f"[{name}-remote]":
|
||||
skip = True
|
||||
elif skip and line.strip().startswith("["):
|
||||
skip = False
|
||||
if not skip:
|
||||
out.append(line)
|
||||
open(path, 'w').writelines(out)
|
||||
PYEOF
|
||||
info "rclone remote [${SPOKE_NAME}-remote] removed from $RCLONE_CONF."
|
||||
else
|
||||
warn "rclone remote [${SPOKE_NAME}-remote] not found in $RCLONE_CONF."
|
||||
fi
|
||||
|
||||
header "Removing SSH Key"
|
||||
read -rp "Remove hub SSH key for $SPOKE_NAME ($KEY_PATH)? [y/N]: " REMOVE_KEY
|
||||
if [[ "${REMOVE_KEY,,}" == "y" ]]; then
|
||||
if [ -f "$KEY_PATH" ]; then
|
||||
rm -f "$KEY_PATH" "$KEY_PATH.pub"
|
||||
info "SSH key removed."
|
||||
else
|
||||
warn "Key not found at $KEY_PATH."
|
||||
fi
|
||||
else
|
||||
info "SSH key left in place."
|
||||
fi
|
||||
|
||||
header "Removing from Registry"
|
||||
(grep -v "^$SPOKE_NAME " "$REGISTRY" || true) > "${REGISTRY}.tmp" && mv "${REGISTRY}.tmp" "$REGISTRY"
|
||||
info "$SPOKE_NAME removed from registry."
|
||||
|
||||
header "Offboarding Complete"
|
||||
echo -e " Spoke ${GREEN}$SPOKE_NAME${NC} has been offboarded."
|
||||
echo ""
|
||||
55
hub/onboard-spoke.sh
Normal file → Executable file
55
hub/onboard-spoke.sh
Normal file → Executable file
@@ -4,21 +4,29 @@ set -euo pipefail
|
||||
RCLONE_CONF="${HOME}/.config/rclone/rclone.conf"
|
||||
SSH_DIR="${HOME}/.ssh"
|
||||
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
echo -e "\033[0;31m[WARNING]\033[0m Running as root — keys will be written to /root/.ssh. Run as armbian instead."
|
||||
exit 1
|
||||
fi
|
||||
mkdir -p "$SSH_DIR"
|
||||
touch "$SSH_DIR/known_hosts"
|
||||
chmod 700 "$SSH_DIR"
|
||||
chmod 600 "$SSH_DIR/known_hosts"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
die "Missing required dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
retry_or_abort() {
|
||||
local test_cmd="$1"
|
||||
local fail_msg="$2"
|
||||
@@ -38,23 +46,30 @@ retry_or_abort() {
|
||||
done
|
||||
}
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
die "Running as root — keys will be written to /root/.ssh. Run as the hub user instead."
|
||||
fi
|
||||
mkdir -p "$SSH_DIR"
|
||||
touch "$SSH_DIR/known_hosts"
|
||||
chmod 700 "$SSH_DIR"
|
||||
chmod 600 "$SSH_DIR/known_hosts"
|
||||
|
||||
check_deps ssh ssh-keygen ssh-keyscan ssh-copy-id rclone
|
||||
|
||||
header "TinyBoard Hub — Onboard New Spoke"
|
||||
|
||||
read -rp "Spoke local user [armbian]: " SPOKE_USER
|
||||
SPOKE_USER="${SPOKE_USER:-armbian}"
|
||||
|
||||
read -rp "Spoke name (e.g. rocky): " SPOKE_NAME
|
||||
[ -n "$SPOKE_NAME" ] || die "Spoke name cannot be empty"
|
||||
|
||||
read -rp "Tunnel port for $SPOKE_NAME: " TUNNEL_PORT
|
||||
[[ "$TUNNEL_PORT" =~ ^[0-9]+$ ]] || die "Invalid port"
|
||||
|
||||
KEY_NAME="armbian-${SPOKE_NAME}-$(date +%Y%m)"
|
||||
KEY_NAME="${SPOKE_USER}-${SPOKE_NAME}-$(date +%Y%m)"
|
||||
KEY_PATH="$SSH_DIR/$KEY_NAME"
|
||||
|
||||
command -v rclone >/dev/null || die "rclone is not installed"
|
||||
mkdir -p "$(dirname "$RCLONE_CONF")"
|
||||
|
||||
header "Checking Tunnel"
|
||||
@@ -65,7 +80,7 @@ echo "$KEYSCAN" >> "$SSH_DIR/known_hosts"
|
||||
|
||||
info "Verifying spoke is reachable on port $TUNNEL_PORT..."
|
||||
retry_or_abort \
|
||||
"ssh -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" armbian@localhost exit" \
|
||||
"ssh -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \
|
||||
"Spoke not reachable on port $TUNNEL_PORT. Make sure the tunnel is up."
|
||||
|
||||
header "Generating Hub SSH Key"
|
||||
@@ -77,14 +92,14 @@ else
|
||||
fi
|
||||
|
||||
header "Copying Hub Key to Spoke"
|
||||
info "Running ssh-copy-id to armbian@localhost:$TUNNEL_PORT..."
|
||||
info "(You will be prompted for the armbian password on the spoke)"
|
||||
ssh-copy-id -i "$KEY_PATH.pub" -p "$TUNNEL_PORT" armbian@localhost
|
||||
info "Running ssh-copy-id to $SPOKE_USER@localhost:$TUNNEL_PORT..."
|
||||
info "(You will be prompted for the $SPOKE_USER password on the spoke)"
|
||||
ssh-copy-id -i "$KEY_PATH.pub" -p "$TUNNEL_PORT" "$SPOKE_USER"@localhost
|
||||
info "Key copied."
|
||||
|
||||
header "Testing Hub -> Spoke Key Auth"
|
||||
retry_or_abort \
|
||||
"ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" armbian@localhost exit" \
|
||||
"ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 -p \"$TUNNEL_PORT\" \"$SPOKE_USER\"@localhost exit" \
|
||||
"Key auth failed. Check authorized_keys on the spoke."
|
||||
info "Key auth to spoke successful."
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
[brie-remote]
|
||||
type = sftp
|
||||
host = localhost
|
||||
port = 11111
|
||||
key_file = /home/armbian/.ssh/armbian-brie-202604
|
||||
shell_type = unix
|
||||
md5sum_command = md5sum
|
||||
sha1sum_command = sha1sum
|
||||
|
||||
#[new-remote]
|
||||
#type = sftp
|
||||
#host = localhost
|
||||
#port = 11112
|
||||
#key_file = /home/armbian/.ssh/a new priv key for tunnel back to new spoke
|
||||
#shell_type = unix
|
||||
#md5sum_command = md5sum
|
||||
#sha1sum_command = sha1sum
|
||||
|
||||
264
hub/setup-hub.sh
Executable file
264
hub/setup-hub.sh
Executable file
@@ -0,0 +1,264 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
die "Missing required dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
check_permissions() {
|
||||
local file="$1"
|
||||
local label="$2"
|
||||
if [ ! -f "$file" ]; then
|
||||
warn "Permission check: $label not found at $file"
|
||||
return
|
||||
fi
|
||||
local perms
|
||||
perms=$(stat -c "%a" "$file" 2>/dev/null || stat -f "%OLp" "$file" 2>/dev/null)
|
||||
if [ -z "$perms" ]; then
|
||||
warn "Could not read permissions for $label ($file)"
|
||||
return
|
||||
fi
|
||||
local world="${perms: -1}"
|
||||
local group="${perms: -2:1}"
|
||||
if [ "$world" != "0" ] || [ "$group" != "0" ]; then
|
||||
warn "UNSAFE PERMISSIONS on $label ($file): $perms — should be 600 or 400"
|
||||
warn "Fixing permissions automatically..."
|
||||
chmod 600 "$file"
|
||||
info "Permissions fixed: $file is now 600"
|
||||
else
|
||||
info "Permissions OK: $label ($file) = $perms"
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run as root"
|
||||
|
||||
check_deps ssh ssh-keygen systemctl useradd groupadd
|
||||
|
||||
header "TinyBoard Hub Setup"
|
||||
|
||||
read -rp "Hub username [armbian]: " HUB_USER
|
||||
HUB_USER="${HUB_USER:-armbian}"
|
||||
|
||||
header "Detecting Package Manager"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
PKG_MANAGER="apt"
|
||||
PKG_INSTALL="apt-get install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
FUSE_PKG="fuse"
|
||||
info "Detected: apt (Debian/Ubuntu)"
|
||||
apt-get update -q
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
PKG_MANAGER="dnf"
|
||||
PKG_INSTALL="dnf install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
FUSE_PKG="fuse"
|
||||
info "Detected: dnf (Fedora/RHEL/Alma/Rocky)"
|
||||
dnf check-update -q || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
PKG_MANAGER="yum"
|
||||
PKG_INSTALL="yum install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
FUSE_PKG="fuse"
|
||||
info "Detected: yum (older RHEL/CentOS)"
|
||||
yum check-update -q || true
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
PKG_MANAGER="pacman"
|
||||
PKG_INSTALL="pacman -S --noconfirm --quiet"
|
||||
OPENSSH_PKG="openssh"
|
||||
FUSE_PKG="fuse3"
|
||||
info "Detected: pacman (Arch)"
|
||||
pacman -Sy --quiet
|
||||
else
|
||||
die "No supported package manager found (apt, dnf, yum, pacman)"
|
||||
fi
|
||||
|
||||
header "Installing Packages"
|
||||
info "Installing curl if missing..."
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
$PKG_INSTALL curl
|
||||
fi
|
||||
|
||||
$PKG_INSTALL "$OPENSSH_PKG" "$FUSE_PKG" git
|
||||
|
||||
if ! command -v rclone >/dev/null 2>&1; then
|
||||
info "Installing rclone..."
|
||||
if [ "$PKG_MANAGER" = "pacman" ]; then
|
||||
$PKG_INSTALL rclone
|
||||
else
|
||||
curl -fsSL https://rclone.org/install.sh | bash
|
||||
fi
|
||||
else
|
||||
warn "rclone already installed, skipping."
|
||||
fi
|
||||
|
||||
header "Armbian User Setup"
|
||||
if id "$HUB_USER" >/dev/null 2>&1; then
|
||||
warn "User '$HUB_USER' already exists, skipping creation."
|
||||
else
|
||||
info "Creating $HUB_USER user..."
|
||||
groupadd -g 1000 "$HUB_USER" 2>/dev/null || true
|
||||
useradd -m -u 1000 -g 1000 -s /bin/bash "$HUB_USER"
|
||||
|
||||
ADDED_TO_GROUP=false
|
||||
if getent group sudo >/dev/null 2>&1; then
|
||||
if usermod -aG sudo "$HUB_USER" 2>/dev/null; then
|
||||
ADDED_TO_GROUP=true
|
||||
fi
|
||||
fi
|
||||
if [ "$ADDED_TO_GROUP" = false ] && getent group wheel >/dev/null 2>&1; then
|
||||
if usermod -aG wheel "$HUB_USER" 2>/dev/null; then
|
||||
ADDED_TO_GROUP=true
|
||||
fi
|
||||
fi
|
||||
if [ "$ADDED_TO_GROUP" = false ]; then
|
||||
warn "Neither sudo nor wheel group found — $HUB_USER user has no sudo access."
|
||||
fi
|
||||
|
||||
info "$HUB_USER user created."
|
||||
echo ""
|
||||
warn "Set a password for the $HUB_USER user:"
|
||||
passwd "$HUB_USER"
|
||||
fi
|
||||
|
||||
ARMBIAN_HOME="/home/$HUB_USER"
|
||||
SSH_DIR="$ARMBIAN_HOME/.ssh"
|
||||
mkdir -p "$SSH_DIR"
|
||||
touch "$SSH_DIR/authorized_keys"
|
||||
chown -R "$HUB_USER":"$HUB_USER" "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
chmod 600 "$SSH_DIR/authorized_keys"
|
||||
|
||||
header "SSH Server Configuration"
|
||||
SSHD_CONF="/etc/ssh/sshd_config"
|
||||
[ -f "$SSHD_CONF" ] || die "sshd_config not found at $SSHD_CONF"
|
||||
|
||||
for DIRECTIVE in "GatewayPorts yes" "AllowTcpForwarding yes"; do
|
||||
KEY="${DIRECTIVE%% *}"
|
||||
if grep -q "^$KEY" "$SSHD_CONF"; then
|
||||
sed -i "s/^$KEY.*/$DIRECTIVE/" "$SSHD_CONF"
|
||||
else
|
||||
echo "$DIRECTIVE" >> "$SSHD_CONF"
|
||||
fi
|
||||
info "$DIRECTIVE set."
|
||||
done
|
||||
|
||||
if systemctl enable ssh 2>/dev/null; then
|
||||
systemctl restart ssh
|
||||
elif systemctl enable sshd 2>/dev/null; then
|
||||
systemctl restart sshd
|
||||
else
|
||||
warn "Could not enable/restart SSH service — please start it manually."
|
||||
fi
|
||||
info "SSH server restarted."
|
||||
|
||||
header "Password Authentication"
|
||||
read -rp "Disable password auth for $HUB_USER and use keys only? [Y/n]: " DISABLE_PASS
|
||||
DISABLE_PASS="${DISABLE_PASS:-y}"
|
||||
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
||||
if [ ! -s "$SSH_DIR/authorized_keys" ]; then
|
||||
warn "No keys found in $SSH_DIR/authorized_keys — skipping password auth disable to avoid lockout."
|
||||
else
|
||||
if grep -q "^PasswordAuthentication" "$SSHD_CONF"; then
|
||||
sed -i "s/^PasswordAuthentication.*/PasswordAuthentication no/" "$SSHD_CONF"
|
||||
else
|
||||
echo "PasswordAuthentication no" >> "$SSHD_CONF"
|
||||
fi
|
||||
if grep -q "^PubkeyAuthentication" "$SSHD_CONF"; then
|
||||
sed -i "s/^PubkeyAuthentication.*/PubkeyAuthentication yes/" "$SSHD_CONF"
|
||||
else
|
||||
echo "PubkeyAuthentication yes" >> "$SSHD_CONF"
|
||||
fi
|
||||
info "Password authentication disabled for $HUB_USER."
|
||||
echo ""
|
||||
warn "Restarting SSH will apply the new settings."
|
||||
warn "If you are connected via SSH, your session may drop."
|
||||
warn "Make sure you can reconnect using your key before continuing."
|
||||
read -rp "Press ENTER to restart SSH or CTRL+C to abort..."
|
||||
if systemctl restart ssh 2>/dev/null; then
|
||||
info "SSH restarted."
|
||||
elif systemctl restart sshd 2>/dev/null; then
|
||||
info "SSH restarted."
|
||||
else
|
||||
warn "Could not restart SSH — please restart it manually."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "Password authentication left enabled."
|
||||
fi
|
||||
|
||||
header "FUSE Configuration"
|
||||
FUSE_CONF="/etc/fuse.conf"
|
||||
if [ -f "$FUSE_CONF" ]; then
|
||||
if grep -q "^#user_allow_other" "$FUSE_CONF"; then
|
||||
sed -i 's/^#user_allow_other/user_allow_other/' "$FUSE_CONF"
|
||||
info "user_allow_other enabled in $FUSE_CONF."
|
||||
elif grep -q "^user_allow_other" "$FUSE_CONF"; then
|
||||
warn "user_allow_other already enabled."
|
||||
else
|
||||
echo "user_allow_other" >> "$FUSE_CONF"
|
||||
info "user_allow_other added to $FUSE_CONF."
|
||||
fi
|
||||
else
|
||||
echo "user_allow_other" > "$FUSE_CONF"
|
||||
info "$FUSE_CONF created with user_allow_other."
|
||||
fi
|
||||
|
||||
groupadd fuse 2>/dev/null || true
|
||||
usermod -aG fuse "$HUB_USER" 2>/dev/null || true
|
||||
info "$HUB_USER added to fuse group."
|
||||
|
||||
header "Rclone Setup"
|
||||
RCLONE_CONF="$ARMBIAN_HOME/.config/rclone/rclone.conf"
|
||||
mkdir -p "$(dirname "$RCLONE_CONF")"
|
||||
chown -R "$HUB_USER":"$HUB_USER" "$ARMBIAN_HOME/.config"
|
||||
|
||||
if [ ! -f "$RCLONE_CONF" ]; then
|
||||
touch "$RCLONE_CONF"
|
||||
chown "$HUB_USER":"$HUB_USER" "$RCLONE_CONF"
|
||||
info "Empty rclone.conf created at $RCLONE_CONF."
|
||||
else
|
||||
warn "rclone.conf already exists, skipping."
|
||||
fi
|
||||
|
||||
header "Permission Checks"
|
||||
info "Checking SSH directory permissions..."
|
||||
check_permissions "$SSH_DIR/authorized_keys" "authorized_keys"
|
||||
check_permissions "$RCLONE_CONF" "rclone.conf"
|
||||
|
||||
header "Mount Point Setup"
|
||||
read -rp "Mount point for spoke filesystems [/mnt/hub]: " MOUNT_POINT
|
||||
MOUNT_POINT="${MOUNT_POINT:-/mnt/hub}"
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
chown "$HUB_USER":"$HUB_USER" "$MOUNT_POINT"
|
||||
info "Mount point created at $MOUNT_POINT."
|
||||
|
||||
header "Hub Setup Complete"
|
||||
echo -e " Hub user: ${GREEN}$HUB_USER${NC}"
|
||||
echo -e " SSH config: ${GREEN}GatewayPorts yes, AllowTcpForwarding yes${NC}"
|
||||
echo -e " FUSE: ${GREEN}user_allow_other enabled${NC}"
|
||||
echo -e " rclone config: ${GREEN}$RCLONE_CONF${NC}"
|
||||
echo -e " Mount point: ${GREEN}$MOUNT_POINT${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Next steps:${NC}"
|
||||
echo " For each spoke that connects, run:"
|
||||
echo " ./setup.sh (choose option 2)"
|
||||
echo ""
|
||||
@@ -1,221 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# hubspoke-helper.sh - Manage hub/spoke rclone mounts
|
||||
# Assumes spoke Docker files exist in ~/autossh-tunnel/
|
||||
# Simplified hub mount uses direct rclone commands (no systemd services)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Configuration (override with env vars if needed)
|
||||
# ------------------------------------------------------------
|
||||
TUNNEL_DIR="${TUNNEL_DIR:-$HOME/tinyboard/spoke}"
|
||||
COMPOSE_FILE="${COMPOSE_FILE:-$TUNNEL_DIR/compose.yaml}"
|
||||
RCLONE_REMOTE="${RCLONE_REMOTE:-brie-remote}"
|
||||
MOUNT_POINT="${MOUNT_POINT:-/mnt/hub/$RCLONE_REMOTE}"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Usage
|
||||
# ------------------------------------------------------------
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $0 {hub|spoke} {action}
|
||||
|
||||
SPOKE ACTIONS (docker-based, no systemd):
|
||||
build Build the autossh image (run once)
|
||||
start Start the tunnel container
|
||||
stop Stop the tunnel container
|
||||
restart Restart the tunnel container
|
||||
status Show container status
|
||||
logs Show container logs
|
||||
show-cmd Show manual autossh command (non-docker)
|
||||
|
||||
HUB ACTIONS (simplified rclone mount - no systemd):
|
||||
install Show simplified setup instructions
|
||||
start Start rclone mount in background (uses nohup)
|
||||
start-background Start rclone mount in background (for crontab)
|
||||
stop Stop rclone mount
|
||||
status Check mount status
|
||||
mount Manual foreground mount (testing)
|
||||
unmount Unmount manually
|
||||
|
||||
EXAMPLES:
|
||||
$0 spoke build
|
||||
$0 spoke start
|
||||
$0 hub install
|
||||
$0 hub start
|
||||
EOF
|
||||
}
|
||||
|
||||
die() {
|
||||
echo "ERROR: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Spoke actions (docker)
|
||||
# ------------------------------------------------------------
|
||||
spoke_build() {
|
||||
if [ ! -f "$COMPOSE_FILE" ]; then
|
||||
die "docker-compose.yaml not found at $COMPOSE_FILE"
|
||||
fi
|
||||
cd "$TUNNEL_DIR"
|
||||
docker build --build-arg UID=$(id -u armbian) --build-arg GID=$(id -g armbian) -t spoke-autossh .
|
||||
echo "Image built. Use '$0 spoke start' to run."
|
||||
}
|
||||
|
||||
spoke_start() {
|
||||
cd "$TUNNEL_DIR"
|
||||
docker-compose up -d
|
||||
}
|
||||
|
||||
spoke_stop() {
|
||||
cd "$TUNNEL_DIR"
|
||||
docker-compose down
|
||||
}
|
||||
|
||||
spoke_restart() {
|
||||
cd "$TUNNEL_DIR"
|
||||
docker-compose restart
|
||||
}
|
||||
|
||||
spoke_status() {
|
||||
docker ps --filter name=spoke-autossh --format "table {{.Names}}\t{{.Status}}"
|
||||
}
|
||||
|
||||
spoke_logs() {
|
||||
cd "$TUNNEL_DIR"
|
||||
docker-compose logs --tail=50 -f
|
||||
}
|
||||
|
||||
spoke_show_cmd() {
|
||||
cat <<EOF
|
||||
Manual autossh command (run on spoke):
|
||||
autossh -M 0 -NT -o "ServerAliveInterval=60" -o "ServerAliveCountMax=3" \\
|
||||
-R 11111:localhost:22 -i ~/.ssh/oilykey2026 armbian@oily.dad
|
||||
EOF
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Hub actions (simplified - no systemd templates)
|
||||
# ------------------------------------------------------------
|
||||
hub_install() {
|
||||
echo "Simplified hub setup:"
|
||||
echo ""
|
||||
echo "1. Ensure /etc/fuse.conf has 'user_allow_other' uncommented:"
|
||||
echo " sudo sed -i 's/^#user_allow_other/user_allow_other/' /etc/fuse.conf"
|
||||
echo ""
|
||||
echo "2. Ensure you're in the 'fuse' group:"
|
||||
echo " sudo usermod -aG fuse $USER"
|
||||
echo " (You may need to log out and back in for this to take effect)"
|
||||
echo ""
|
||||
echo "3. Create mount point directory:"
|
||||
echo " mkdir -p \"$MOUNT_POINT\""
|
||||
echo ""
|
||||
echo "4. Test manual mount:"
|
||||
echo " $0 hub mount"
|
||||
echo ""
|
||||
echo "5. For auto-start, consider adding to crontab with @reboot:"
|
||||
echo " crontab -e"
|
||||
echo " Add: @reboot $0 hub start-background"
|
||||
echo ""
|
||||
echo "Note: This simplified version doesn't use systemd services."
|
||||
}
|
||||
|
||||
hub_start() {
|
||||
echo "Starting rclone mount in background..."
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
nohup rclone mount "${RCLONE_REMOTE}:" "$MOUNT_POINT" \
|
||||
--config "${HOME}/.config/rclone/rclone.conf" \
|
||||
--vfs-cache-mode writes \
|
||||
--allow-other \
|
||||
--daemon >/dev/null 2>&1 &
|
||||
echo "Mount started in background (PID: $!)"
|
||||
echo "Check status with: $0 hub status"
|
||||
}
|
||||
|
||||
hub_start_background() {
|
||||
# Internal function for crontab/auto-start
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
rclone mount "${RCLONE_REMOTE}:" "$MOUNT_POINT" \
|
||||
--config "${HOME}/.config/rclone/rclone.conf" \
|
||||
--vfs-cache-mode writes \
|
||||
--allow-other \
|
||||
--daemon
|
||||
}
|
||||
|
||||
hub_stop() {
|
||||
echo "Stopping rclone mount..."
|
||||
if hub_unmount; then
|
||||
echo "Mount stopped."
|
||||
else
|
||||
echo "Could not unmount. Trying force unmount..."
|
||||
fusermount -uz "$MOUNT_POINT" 2>/dev/null && echo "Force unmounted." || echo "Still could not unmount."
|
||||
fi
|
||||
}
|
||||
|
||||
hub_status() {
|
||||
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
|
||||
echo "Mount point $MOUNT_POINT is mounted."
|
||||
mount | grep "$MOUNT_POINT"
|
||||
else
|
||||
echo "Mount point $MOUNT_POINT is NOT mounted."
|
||||
echo "Check if rclone process is running:"
|
||||
pgrep -af rclone || echo "No rclone mount processes found."
|
||||
fi
|
||||
}
|
||||
|
||||
hub_mount() {
|
||||
mkdir -p "$MOUNT_POINT"
|
||||
echo "Mounting in foreground. Press Ctrl+C to unmount."
|
||||
rclone mount "${RCLONE_REMOTE}:" "$MOUNT_POINT" \
|
||||
--config "${HOME}/.config/rclone/rclone.conf" \
|
||||
--vfs-cache-mode writes \
|
||||
--allow-other
|
||||
}
|
||||
|
||||
hub_unmount() {
|
||||
fusermount -u "$MOUNT_POINT" 2>/dev/null && echo "Unmounted." || echo "Not mounted."
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Dispatch
|
||||
# ------------------------------------------------------------
|
||||
if [ $# -lt 2 ]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ROLE="$1"
|
||||
ACTION="$2"
|
||||
|
||||
case "$ROLE" in
|
||||
spoke)
|
||||
case "$ACTION" in
|
||||
build) spoke_build ;;
|
||||
start) spoke_start ;;
|
||||
stop) spoke_stop ;;
|
||||
restart) spoke_restart ;;
|
||||
status) spoke_status ;;
|
||||
logs) spoke_logs ;;
|
||||
show-cmd) spoke_show_cmd ;;
|
||||
*) die "Unknown action for spoke: $ACTION" ;;
|
||||
esac
|
||||
;;
|
||||
hub)
|
||||
case "$ACTION" in
|
||||
install) hub_install ;;
|
||||
start) hub_start ;;
|
||||
start-background) hub_start_background ;;
|
||||
stop) hub_stop ;;
|
||||
status) hub_status ;;
|
||||
mount) hub_mount ;;
|
||||
unmount) hub_unmount ;;
|
||||
*) die "Unknown action for hub: $ACTION" ;;
|
||||
esac
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
19
setup.sh
Normal file → Executable file
19
setup.sh
Normal file → Executable file
@@ -15,12 +15,20 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
header "TinyBoard Setup"
|
||||
echo ""
|
||||
echo " 0) Reconfigure network"
|
||||
echo " 1) Set up this device as a new spoke"
|
||||
echo " 2) Onboard a new spoke from the hub"
|
||||
echo " 3) Offboard a spoke from the hub"
|
||||
echo " 4) Set up this device as a new hub"
|
||||
echo ""
|
||||
read -rp "Choose [1/2]: " CHOICE
|
||||
read -rp "Choose [0/1/2/3/4]: " CHOICE
|
||||
|
||||
case "$CHOICE" in
|
||||
0)
|
||||
[ "$(id -u)" -eq 0 ] || die "Network reconfiguration must be run as root"
|
||||
info "Starting network reconfiguration..."
|
||||
exec "$SCRIPT_DIR/spoke/setup-network.sh"
|
||||
;;
|
||||
1)
|
||||
[ "$(id -u)" -eq 0 ] || die "Spoke setup must be run as root"
|
||||
info "Starting spoke setup..."
|
||||
@@ -30,6 +38,15 @@ case "$CHOICE" in
|
||||
info "Starting hub onboarding..."
|
||||
exec "$SCRIPT_DIR/hub/onboard-spoke.sh"
|
||||
;;
|
||||
3)
|
||||
info "Starting hub offboarding..."
|
||||
exec "$SCRIPT_DIR/hub/offboard-spoke.sh"
|
||||
;;
|
||||
4)
|
||||
[ "$(id -u)" -eq 0 ] || die "Hub setup must be run as root"
|
||||
info "Starting hub setup..."
|
||||
exec "$SCRIPT_DIR/hub/setup-hub.sh"
|
||||
;;
|
||||
*)
|
||||
die "Invalid choice"
|
||||
;;
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Need armbian-config?
|
||||
|
||||
apt install -y vim
|
||||
apt install -y autossh
|
||||
apt install -y docker.io docker-cli docker-compose
|
||||
usermod -aG docker armbian
|
||||
@@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copy this along with .not_logged_in_yet to armbian root dir, then run after successful login
|
||||
|
||||
# Refresh: extract MAC address of wlan0
|
||||
MAC=$(netplan status -f json | jq -r '.wlan0.macaddress')
|
||||
|
||||
# Check that we actually got a MAC address
|
||||
if [[ -z "$MAC" ]]; then
|
||||
echo "Error: Could not retrieve MAC address from netplan." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Detected MAC address: $MAC"
|
||||
|
||||
# Assign cheese hostname based on MAC address
|
||||
case "$MAC" in
|
||||
38:9c:80:46:26:c8) # ← Replace with your first real MAC
|
||||
HOSTNAME="brie"
|
||||
;;
|
||||
68:f8:ea:22:e1:3d) # ← Replace with your second real MAC
|
||||
HOSTNAME="gouda"
|
||||
;;
|
||||
99:88:77:66:55:44) # ← Replace with your third real MAC
|
||||
HOSTNAME="camembert"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown MAC address: $MAC ... hostname not changed." >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Setting hostname to: $HOSTNAME"
|
||||
sudo hostnamectl set-hostname "$HOSTNAME"
|
||||
|
||||
# Optional: also update /etc/hostname (hostnamectl usually does this, but to be safe)
|
||||
echo "$HOSTNAME" | sudo tee /etc/hostname >/dev/null
|
||||
|
||||
echo "Hostname changed. Reboot or start a new shell to see the change."
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to clean sensitive WiFi credentials and passwords from configuration files
|
||||
# Usage: ./clean_sensitive.sh [filename]
|
||||
|
||||
FILE="${1:-/home/finn/code/tinyboard/armb-not_logged_in_yet}"
|
||||
|
||||
echo "Cleaning sensitive data from: $FILE"
|
||||
|
||||
# Clean WiFi SSID (both commented and uncommented lines)
|
||||
sed -i 's/^\(#*PRESET_NET_WIFI_SSID=\).*$/\1"[REDACTED]"/' "$FILE"
|
||||
|
||||
# Clean WiFi KEY (both commented and uncommented lines)
|
||||
sed -i 's/^\(#*PRESET_NET_WIFI_KEY=\).*$/\1"[REDACTED]"/' "$FILE"
|
||||
|
||||
# Clean root password
|
||||
sed -i 's/^\(PRESET_ROOT_PASSWORD=\).*$/\1"[REDACTED]"/' "$FILE"
|
||||
|
||||
# Clean user password
|
||||
sed -i 's/^\(PRESET_USER_PASSWORD=\).*$/\1"[REDACTED]"/' "$FILE"
|
||||
|
||||
echo "wiped fields"
|
||||
56
setup-network.sh → spoke/setup-network.sh
Normal file → Executable file
56
setup-network.sh → spoke/setup-network.sh
Normal file → Executable file
@@ -12,8 +12,22 @@ warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
die "Missing required dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run as root"
|
||||
|
||||
check_deps ip netplan systemctl ping
|
||||
|
||||
header "TinyBoard Network Setup"
|
||||
|
||||
info "Available interfaces:"
|
||||
@@ -73,7 +87,7 @@ echo ""
|
||||
|
||||
NETPLAN_FILE=$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)
|
||||
read -rp "Netplan file to update [${NETPLAN_FILE}]: " INPUT_FILE
|
||||
NETPLAN_FILE="${INPUT_FILE:-$INPUT_FILE}"
|
||||
NETPLAN_FILE="${INPUT_FILE:-$NETPLAN_FILE}"
|
||||
NETPLAN_FILE="${NETPLAN_FILE:-$(ls /etc/netplan/*.yaml 2>/dev/null | head -1)}"
|
||||
[ -n "$NETPLAN_FILE" ] || die "No netplan file specified"
|
||||
|
||||
@@ -100,13 +114,16 @@ if $IS_WIFI; then
|
||||
else
|
||||
WIFI_SSID="$CURRENT_SSID"
|
||||
WIFI_PASS=$(grep -A2 "\"${WIFI_SSID}\"" "$NETPLAN_FILE" 2>/dev/null | grep password | awk -F': ' '{print $2}' | tr -d '"' || true)
|
||||
[ -n "$WIFI_PASS" ] || die "Could not extract WiFi password from existing config — please re-enter credentials."
|
||||
fi
|
||||
fi
|
||||
|
||||
header "Writing Netplan Config"
|
||||
BACKUP_FILE=""
|
||||
if [ -f "$NETPLAN_FILE" ]; then
|
||||
cp "$NETPLAN_FILE" "${NETPLAN_FILE}.bak"
|
||||
info "Backup saved to ${NETPLAN_FILE}.bak"
|
||||
BACKUP_FILE="/root/$(basename "${NETPLAN_FILE}").bak"
|
||||
cp "$NETPLAN_FILE" "$BACKUP_FILE"
|
||||
info "Backup saved to $BACKUP_FILE"
|
||||
fi
|
||||
|
||||
if $IS_WIFI; then
|
||||
@@ -141,18 +158,37 @@ network:
|
||||
via: ${GATEWAY}
|
||||
nameservers:
|
||||
addresses:
|
||||
$(printf '%b' "$DNS_YAML")NETEOF
|
||||
$(printf '%b' "$DNS_YAML")
|
||||
NETEOF
|
||||
fi
|
||||
|
||||
info "Netplan config written to $NETPLAN_FILE"
|
||||
|
||||
header "Applying Configuration"
|
||||
warn "Testing netplan config..."
|
||||
if netplan try --timeout 10 2>/dev/null; then
|
||||
info "Netplan config applied successfully."
|
||||
warn "Applying netplan config — will revert automatically if network is lost..."
|
||||
netplan apply
|
||||
|
||||
CONNECTED=false
|
||||
for i in $(seq 1 6); do
|
||||
sleep 5
|
||||
if ping -c 1 -W 2 "$GATEWAY" >/dev/null 2>&1; then
|
||||
CONNECTED=true
|
||||
break
|
||||
fi
|
||||
warn "Network check $i/6 failed, retrying..."
|
||||
done
|
||||
|
||||
if $CONNECTED; then
|
||||
info "Network connectivity confirmed — config applied permanently."
|
||||
else
|
||||
warn "netplan try timed out or failed — applying anyway..."
|
||||
netplan apply
|
||||
warn "No network connectivity detected after 30 seconds — reverting to backup config."
|
||||
if [ -f "$BACKUP_FILE" ]; then
|
||||
cp "$BACKUP_FILE" "$NETPLAN_FILE"
|
||||
netplan apply
|
||||
die "Config reverted to backup. Check your settings and try again."
|
||||
else
|
||||
die "No backup found to revert to. Restore $NETPLAN_FILE manually."
|
||||
fi
|
||||
fi
|
||||
|
||||
STATIC_ADDR="${STATIC_IP%%/*}"
|
||||
@@ -161,6 +197,6 @@ echo -e "${YELLOW}════════════════════
|
||||
echo -e "${YELLOW} Network reconfigured.${NC}"
|
||||
echo -e "${YELLOW} If you are connected via SSH, your session${NC}"
|
||||
echo -e "${YELLOW} may drop. Reconnect to: ${STATIC_ADDR}${NC}"
|
||||
echo -e "${YELLOW} Then run: sudo ./setup-spoke.sh${NC}"
|
||||
echo -e "${YELLOW} Then run: cd .. && ./setup.sh${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
236
spoke/setup-spoke.sh
Normal file → Executable file
236
spoke/setup-spoke.sh
Normal file → Executable file
@@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
HUB_HOST="oily.dad"
|
||||
HUB_USER="armbian"
|
||||
ARMBIAN_HOME="/home/armbian"
|
||||
SSH_DIR="$ARMBIAN_HOME/.ssh"
|
||||
HUB_HOST=""
|
||||
HUB_USER=""
|
||||
SPOKE_USER=""
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SPOKE_DIR="$SCRIPT_DIR/spoke"
|
||||
SPOKE_DIR="$SCRIPT_DIR"
|
||||
COMPOSE="$SPOKE_DIR/compose.yaml"
|
||||
START_PORT=11111
|
||||
|
||||
@@ -16,6 +15,23 @@ YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
|
||||
check_deps() {
|
||||
local missing=()
|
||||
for cmd in "$@"; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
missing+=("$cmd")
|
||||
fi
|
||||
done
|
||||
if [ ${#missing[@]} -gt 0 ]; then
|
||||
die "Missing required dependencies: ${missing[*]}"
|
||||
fi
|
||||
}
|
||||
|
||||
retry_or_abort() {
|
||||
local test_cmd="$1"
|
||||
local fail_msg="$2"
|
||||
@@ -35,25 +51,118 @@ retry_or_abort() {
|
||||
done
|
||||
}
|
||||
|
||||
info() { echo -e "${GREEN}[+]${NC} $*"; }
|
||||
warn() { echo -e "${YELLOW}[!]${NC} $*"; }
|
||||
die() { echo -e "${RED}[ERROR]${NC} $*" >&2; exit 1; }
|
||||
header() { echo -e "\n${CYAN}══════════════════════════════════════════${NC}"; echo -e "${CYAN} $*${NC}"; echo -e "${CYAN}══════════════════════════════════════════${NC}"; }
|
||||
check_permissions() {
|
||||
local file="$1"
|
||||
local label="$2"
|
||||
if [ ! -f "$file" ]; then
|
||||
warn "Permission check: $label not found at $file"
|
||||
return
|
||||
fi
|
||||
local perms
|
||||
perms=$(stat -c "%a" "$file" 2>/dev/null || stat -f "%OLp" "$file" 2>/dev/null)
|
||||
if [ -z "$perms" ]; then
|
||||
warn "Could not read permissions for $label ($file)"
|
||||
return
|
||||
fi
|
||||
local world="${perms: -1}"
|
||||
local group="${perms: -2:1}"
|
||||
if [ "$world" != "0" ] || [ "$group" != "0" ]; then
|
||||
warn "UNSAFE PERMISSIONS on $label ($file): $perms — should be 600 or 400"
|
||||
warn "Fixing permissions automatically..."
|
||||
chmod 600 "$file"
|
||||
info "Permissions fixed: $file is now 600"
|
||||
else
|
||||
info "Permissions OK: $label ($file) = $perms"
|
||||
fi
|
||||
}
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || die "Run as root"
|
||||
|
||||
check_deps ip ssh ssh-keygen ssh-keyscan systemctl hostnamectl
|
||||
|
||||
read -rp "Hub hostname [oily.dad]: " HUB_HOST
|
||||
HUB_HOST="${HUB_HOST:-oily.dad}"
|
||||
read -rp "Hub SSH user [armbian]: " HUB_USER
|
||||
HUB_USER="${HUB_USER:-armbian}"
|
||||
read -rp "Spoke local user [armbian]: " SPOKE_USER
|
||||
SPOKE_USER="${SPOKE_USER:-armbian}"
|
||||
ARMBIAN_HOME="/home/$SPOKE_USER"
|
||||
SSH_DIR="$ARMBIAN_HOME/.ssh"
|
||||
|
||||
header "TinyBoard Spoke Setup"
|
||||
|
||||
info "Installing packages..."
|
||||
apt-get update -q
|
||||
apt-get install -y -q vim autossh docker.io docker-compose-plugin git openssh-server
|
||||
header "Detecting Package Manager"
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
PKG_MANAGER="apt"
|
||||
PKG_INSTALL="apt-get install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
AUTOSSH_PKG="autossh"
|
||||
info "Detected: apt (Debian/Ubuntu)"
|
||||
apt-get update -q
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
PKG_MANAGER="dnf"
|
||||
PKG_INSTALL="dnf install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
AUTOSSH_PKG="autossh"
|
||||
info "Detected: dnf (Fedora/RHEL/Alma/Rocky)"
|
||||
dnf check-update -q || true
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
PKG_MANAGER="yum"
|
||||
PKG_INSTALL="yum install -y -q"
|
||||
OPENSSH_PKG="openssh-server"
|
||||
AUTOSSH_PKG="autossh"
|
||||
info "Detected: yum (older RHEL/CentOS)"
|
||||
yum check-update -q || true
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
PKG_MANAGER="pacman"
|
||||
PKG_INSTALL="pacman -S --noconfirm --quiet"
|
||||
OPENSSH_PKG="openssh"
|
||||
AUTOSSH_PKG="autossh"
|
||||
info "Detected: pacman (Arch)"
|
||||
pacman -Sy --quiet
|
||||
else
|
||||
die "No supported package manager found (apt, dnf, yum, pacman)"
|
||||
fi
|
||||
|
||||
info "Adding armbian to docker group..."
|
||||
usermod -aG docker armbian 2>/dev/null || true
|
||||
header "Installing Packages"
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
$PKG_INSTALL curl
|
||||
fi
|
||||
|
||||
$PKG_INSTALL vim "$AUTOSSH_PKG" "$OPENSSH_PKG" git
|
||||
|
||||
info "Installing Docker..."
|
||||
if ! command -v docker >/dev/null 2>&1; then
|
||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||
$PKG_INSTALL docker.io docker-compose-plugin
|
||||
else
|
||||
curl -fsSL https://get.docker.com | bash
|
||||
fi
|
||||
else
|
||||
warn "Docker already installed, skipping."
|
||||
fi
|
||||
|
||||
if ! docker compose version >/dev/null 2>&1; then
|
||||
if [ "$PKG_MANAGER" = "apt" ]; then
|
||||
$PKG_INSTALL docker-compose-plugin
|
||||
else
|
||||
warn "docker compose not available — Docker install script should have included it."
|
||||
fi
|
||||
fi
|
||||
|
||||
info "Adding $SPOKE_USER to docker group..."
|
||||
usermod -aG docker "$SPOKE_USER" 2>/dev/null || true
|
||||
|
||||
info "Enabling SSH server..."
|
||||
systemctl enable ssh
|
||||
systemctl start ssh
|
||||
if systemctl enable ssh 2>/dev/null; then
|
||||
systemctl start ssh
|
||||
elif systemctl enable sshd 2>/dev/null; then
|
||||
systemctl start sshd
|
||||
else
|
||||
warn "Could not enable SSH service — please start it manually."
|
||||
fi
|
||||
|
||||
SSHD_CONF="/etc/ssh/sshd_config"
|
||||
|
||||
header "Hostname Setup"
|
||||
CURRENT_HOSTNAME=$(hostname)
|
||||
@@ -73,43 +182,44 @@ read -rp "Choose [1/2]: " KEY_CHOICE
|
||||
|
||||
case "$KEY_CHOICE" in
|
||||
1)
|
||||
KEY_NAME="oilykey"
|
||||
read -rp "Key name [hubkey]: " KEY_NAME
|
||||
KEY_NAME="${KEY_NAME:-hubkey}"
|
||||
KEY_PATH="$SSH_DIR/$KEY_NAME"
|
||||
mkdir -p "$SSH_DIR"
|
||||
chown armbian:armbian "$SSH_DIR"
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
|
||||
if [ -f "$KEY_PATH" ]; then
|
||||
warn "Key $KEY_PATH already exists, using it."
|
||||
else
|
||||
info "Generating new ED25519 key..."
|
||||
sudo -u armbian ssh-keygen -t ed25519 -f "$KEY_PATH" -N ""
|
||||
chown armbian:armbian "$KEY_PATH" "$KEY_PATH.pub"
|
||||
sudo -u "$SPOKE_USER" ssh-keygen -t ed25519 -f "$KEY_PATH" -N ""
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$KEY_PATH" "$KEY_PATH.pub"
|
||||
chmod 600 "$KEY_PATH"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
||||
echo -e "${YELLOW} Send this public key to finn (oily.dad owner)${NC}"
|
||||
echo -e "${YELLOW} and ask him to add it to armbian@oily.dad authorized_keys:${NC}"
|
||||
echo -e "${YELLOW} Send this public key to the hub owner${NC}"
|
||||
echo -e "${YELLOW} and ask them to add it to ${HUB_USER}@${HUB_HOST} authorized_keys:${NC}"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
||||
cat "$KEY_PATH.pub"
|
||||
echo -e "${YELLOW}══════════════════════════════════════════${NC}"
|
||||
echo ""
|
||||
read -rp "Press ENTER once finn has added the key to oily.dad..."
|
||||
read -rp "Press ENTER once the key has been added to ${HUB_HOST}..."
|
||||
;;
|
||||
2)
|
||||
read -rp "Enter a name for the key file (e.g. oilykey): " KEY_NAME
|
||||
KEY_NAME="${KEY_NAME:-oilykey}"
|
||||
read -rp "Enter a name for the key file [hubkey]: " KEY_NAME
|
||||
KEY_NAME="${KEY_NAME:-hubkey}"
|
||||
KEY_PATH="$SSH_DIR/$KEY_NAME"
|
||||
mkdir -p "$SSH_DIR"
|
||||
chown armbian:armbian "$SSH_DIR"
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$SSH_DIR"
|
||||
chmod 700 "$SSH_DIR"
|
||||
|
||||
echo "Paste the private key content below, then press ENTER and CTRL+D:"
|
||||
KEY_CONTENT=$(cat | tr -d '\r')
|
||||
printf '%s\n' "$KEY_CONTENT" > "$KEY_PATH"
|
||||
chown armbian:armbian "$KEY_PATH"
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$KEY_PATH"
|
||||
chmod 600 "$KEY_PATH"
|
||||
info "Key saved to $KEY_PATH"
|
||||
;;
|
||||
@@ -118,23 +228,65 @@ case "$KEY_CHOICE" in
|
||||
;;
|
||||
esac
|
||||
|
||||
header "Password Authentication"
|
||||
read -rp "Disable password auth for $SPOKE_USER and use keys only? [Y/n]: " DISABLE_PASS
|
||||
DISABLE_PASS="${DISABLE_PASS:-y}"
|
||||
if [[ "${DISABLE_PASS,,}" == "y" ]]; then
|
||||
if [ ! -f "$KEY_PATH" ]; then
|
||||
warn "No key found at $KEY_PATH — skipping password auth disable to avoid lockout."
|
||||
else
|
||||
if grep -q "^PasswordAuthentication" "$SSHD_CONF"; then
|
||||
sed -i "s/^PasswordAuthentication.*/PasswordAuthentication no/" "$SSHD_CONF"
|
||||
else
|
||||
echo "PasswordAuthentication no" >> "$SSHD_CONF"
|
||||
fi
|
||||
if grep -q "^PubkeyAuthentication" "$SSHD_CONF"; then
|
||||
sed -i "s/^PubkeyAuthentication.*/PubkeyAuthentication yes/" "$SSHD_CONF"
|
||||
else
|
||||
echo "PubkeyAuthentication yes" >> "$SSHD_CONF"
|
||||
fi
|
||||
info "Password authentication disabled for $SPOKE_USER."
|
||||
echo ""
|
||||
warn "Restarting SSH will apply the new settings."
|
||||
warn "If you are connected via SSH, your session may drop."
|
||||
warn "Make sure you can reconnect using your key before continuing."
|
||||
read -rp "Press ENTER to restart SSH or CTRL+C to abort..."
|
||||
if systemctl restart ssh 2>/dev/null; then
|
||||
info "SSH restarted."
|
||||
elif systemctl restart sshd 2>/dev/null; then
|
||||
info "SSH restarted."
|
||||
else
|
||||
warn "Could not restart SSH — please restart it manually."
|
||||
fi
|
||||
fi
|
||||
else
|
||||
info "Password authentication left enabled."
|
||||
fi
|
||||
|
||||
info "Checking SSH key permissions..."
|
||||
check_permissions "$KEY_PATH" "spoke SSH private key"
|
||||
if [ -f "$KEY_PATH.pub" ]; then
|
||||
check_permissions "$KEY_PATH.pub" "spoke SSH public key"
|
||||
fi
|
||||
|
||||
info "Scanning hub host key..."
|
||||
sudo -u armbian touch "$SSH_DIR/known_hosts"
|
||||
chown armbian:armbian "$SSH_DIR/known_hosts"
|
||||
sudo -u "$SPOKE_USER" touch "$SSH_DIR/known_hosts"
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$SSH_DIR/known_hosts"
|
||||
chmod 600 "$SSH_DIR/known_hosts"
|
||||
sudo -u armbian ssh-keyscan -H "$HUB_HOST" >> "$SSH_DIR/known_hosts" 2>/dev/null
|
||||
sudo -u "$SPOKE_USER" ssh-keyscan -H "$HUB_HOST" >> "$SSH_DIR/known_hosts" 2>/dev/null
|
||||
check_permissions "$SSH_DIR/known_hosts" "known_hosts"
|
||||
|
||||
header "Testing SSH Connection"
|
||||
info "Testing connection to $HUB_HOST..."
|
||||
retry_or_abort \
|
||||
"sudo -u armbian ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 \"$HUB_USER@$HUB_HOST\" exit" \
|
||||
"SSH connection to $HUB_HOST failed. Check that finn added your public key."
|
||||
"sudo -u \"$SPOKE_USER\" ssh -i \"$KEY_PATH\" -o BatchMode=yes -o ConnectTimeout=10 \"$HUB_USER@$HUB_HOST\" exit" \
|
||||
"SSH connection to $HUB_HOST failed. Check that the hub owner added your public key."
|
||||
|
||||
header "Finding Available Tunnel Port"
|
||||
info "Scanning for a free port on $HUB_HOST starting from $START_PORT..."
|
||||
TUNNEL_PORT=""
|
||||
for PORT in $(seq "$START_PORT" $((START_PORT + 20))); do
|
||||
RESULT=$(sudo -u armbian ssh -i "$KEY_PATH" "$HUB_USER@$HUB_HOST" "ss -tlnp | grep :$PORT" 2>/dev/null || true)
|
||||
RESULT=$(sudo -u "$SPOKE_USER" ssh -i "$KEY_PATH" "$HUB_USER@$HUB_HOST" "ss -tlnp | grep :$PORT" 2>/dev/null || true)
|
||||
if [ -z "$RESULT" ]; then
|
||||
TUNNEL_PORT="$PORT"
|
||||
info "Port $TUNNEL_PORT is available."
|
||||
@@ -144,14 +296,14 @@ for PORT in $(seq "$START_PORT" $((START_PORT + 20))); do
|
||||
fi
|
||||
done
|
||||
|
||||
[ -n "$TUNNEL_PORT" ] || die "Could not find a free port between $START_PORT and $((START_PORT + 20)). Ask finn to free up a port."
|
||||
[ -n "$TUNNEL_PORT" ] || die "Could not find a free port between $START_PORT and $((START_PORT + 20)). Ask the hub owner to free up a port."
|
||||
|
||||
header "Configuring compose.yaml"
|
||||
info "Setting port to $TUNNEL_PORT and key to $KEY_NAME..."
|
||||
|
||||
sed -i "s|-R [0-9]*:localhost:22|-R ${TUNNEL_PORT}:localhost:22|g" "$COMPOSE"
|
||||
sed -i "s|-i /home/armbian/.ssh/[^ ]*|-i /home/armbian/.ssh/${KEY_NAME}|g" "$COMPOSE"
|
||||
sed -i "s|/home/armbian/.ssh/oilykey[^:]*:/home/armbian/.ssh/oilykey[^:]*|${SSH_DIR}/${KEY_NAME}:${SSH_DIR}/${KEY_NAME}|g" "$COMPOSE"
|
||||
sed -i "s|-i /home/[^ ]*/\.ssh/[^ ]*|-i ${SSH_DIR}/${KEY_NAME}|g" "$COMPOSE"
|
||||
sed -i "s|/home/[^/]*/\.ssh/[^:]*:/home/[^/]*/\.ssh/[^:]*|${SSH_DIR}/${KEY_NAME}:${SSH_DIR}/${KEY_NAME}|g" "$COMPOSE"
|
||||
sed -i "s|container_name: spoke-autossh|container_name: ${SPOKE_NAME}-autossh|g" "$COMPOSE"
|
||||
sed -i "s|container_name: spoke-syncthing|container_name: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
||||
sed -i "s|hostname: spoke-syncthing|hostname: ${SPOKE_NAME}-syncthing|g" "$COMPOSE"
|
||||
@@ -159,13 +311,13 @@ sed -i '/^version:/d' "$COMPOSE"
|
||||
|
||||
SYNCTHING_MOUNT="$ARMBIAN_HOME/st"
|
||||
mkdir -p "$SYNCTHING_MOUNT"
|
||||
chown armbian:armbian "$SYNCTHING_MOUNT"
|
||||
chown "$SPOKE_USER":"$SPOKE_USER" "$SYNCTHING_MOUNT"
|
||||
|
||||
header "Building Docker Image"
|
||||
cd "$SPOKE_DIR"
|
||||
docker build \
|
||||
--build-arg UID="$(id -u armbian)" \
|
||||
--build-arg GID="$(id -g armbian)" \
|
||||
--build-arg UID="$(id -u "$SPOKE_USER")" \
|
||||
--build-arg GID="$(id -g "$SPOKE_USER")" \
|
||||
-t spoke-autossh .
|
||||
|
||||
header "Starting Containers"
|
||||
@@ -187,20 +339,20 @@ echo -e " Spoke name: ${GREEN}$SPOKE_NAME${NC}"
|
||||
echo -e " Tunnel port: ${GREEN}$TUNNEL_PORT${NC} on $HUB_HOST"
|
||||
echo -e " SSH key: ${GREEN}$KEY_PATH${NC}"
|
||||
echo ""
|
||||
echo -e "${YELLOW}Finn needs to do the following on oily.dad:${NC}"
|
||||
echo -e "${YELLOW}The hub owner needs to do the following on ${HUB_HOST}:${NC}"
|
||||
echo ""
|
||||
echo " 1. Generate a hub->spoke key:"
|
||||
echo " ssh-keygen -t ed25519 -f ~/.ssh/armbian-${SPOKE_NAME}-$(date +%Y%m)"
|
||||
echo ""
|
||||
echo " 2. Copy it to this spoke through the tunnel:"
|
||||
echo " ssh-copy-id -i ~/.ssh/armbian-${SPOKE_NAME}-$(date +%Y%m).pub -p $TUNNEL_PORT armbian@localhost"
|
||||
echo " ssh-copy-id -i ~/.ssh/armbian-${SPOKE_NAME}-$(date +%Y%m).pub -p $TUNNEL_PORT ${HUB_USER}@localhost"
|
||||
echo ""
|
||||
echo " 3. Add an rclone remote in ~/.config/rclone/rclone.conf:"
|
||||
echo " [${SPOKE_NAME}-remote]"
|
||||
echo " type = sftp"
|
||||
echo " host = localhost"
|
||||
echo " port = $TUNNEL_PORT"
|
||||
echo " key_file = /home/armbian/.ssh/armbian-${SPOKE_NAME}-$(date +%Y%m)"
|
||||
echo " key_file = /home/$HUB_USER/.ssh/armbian-${SPOKE_NAME}-$(date +%Y%m)"
|
||||
echo " shell_type = unix"
|
||||
echo " md5sum_command = md5sum"
|
||||
echo " sha1sum_command = sha1sum"
|
||||
|
||||
Reference in New Issue
Block a user