Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,19 @@ Documentation: **[English](docs/en/README.md)** · **[Русский](docs/ru/RE

## Quick start

To configure the Debian/Ubuntu server you are logged into, run the interactive installer:

```shell
curl -fsSL https://raw.githubusercontent.com/andre487/MegaProxyServer/main/install.sh | sudo bash
```

It installs dependencies and starts the setup wizard. Keep your SSH session open and save the
administrative key on your computer before confirming provisioning. See the
[English](docs/en/README.md#install-on-the-server) or
[Russian](docs/ru/README.md#установка-на-самом-сервере) walkthrough.

### Manage servers from another machine

Requirements: macOS or Linux, Python 3.12+, `uv`, OpenSSH, and a Debian or Ubuntu server reachable through a
sudo-capable SSH account.

Expand Down
50 changes: 50 additions & 0 deletions docs/en/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,56 @@ Public-IP ACME certificates require Certbot 5.4+ and Let's Encrypt's short-lived

## First deployment

### Install on the server

Connect to Debian/Ubuntu with an interactive terminal (`ssh -t`) or use the server console.
You need root or sudo, curl and internet access. Install curl if necessary:

```shell
sudo apt-get update && sudo apt-get install -y curl ca-certificates
```

Start the installer (omit sudo when logged in as root):

```shell
curl -fsSL https://raw.githubusercontent.com/andre487/MegaProxyServer/main/install.sh | sudo bash
```

To inspect it first, download with `curl -fsSL URL -o install.sh`, review the file and run
`sudo bash install.sh`, using the URL above.

1. Confirm dependency installation. The checkout lives in `/opt/megaproxy-server`; Python and
project dependencies are installed automatically.
2. Enter a host name, its **public** IP or DNS name, administrator and current SSH port. Choose or
generate an administrative key, then select HTTPS and/or SSH, certificates and proxy users.
For domain ACME, point DNS at this server beforehand. Open the required provider firewall ports.
3. Set and save an Ansible Vault password; subsequent invocations will need it.
4. Copy the displayed administrative private key to your computer before applying. With existing
root access, use another local terminal: `scp root@SERVER:/PATH/TO/KEY ./megaproxy-admin`, then
`chmod 600 ./megaproxy-admin`. With a sudo account, transfer securely using sudo to read the
file; do not make it publicly readable on the server.
5. Confirm that the key is saved. Setup creates the administrator locally, checks a fresh SSH login
and sudo through loopback, disables root/password administrative login, configures services and
the host firewall, verifies provisioning and exports client configurations.
6. Keep the original session open and test login **from your computer**:
`ssh -i ./megaproxy-admin -p PORT ADMIN@SERVER`. Securely retrieve
`/opt/megaproxy-server/.generated/configs/MegaProxy.json` using sudo access.

If you cancelled before applying or setup failed, rerun the installer or resume with:

```shell
sudo /opt/megaproxy-server/mega-proxy setup-local
```

Existing inventory is reused; the installer does not automatically update an existing checkout.
For maintenance, run `sudo /opt/megaproxy-server/mega-proxy` with the desired command.
Inventory, keys and exports stay on the server; keep private backups. In this mode `local: true`
routes administrative SSH through loopback while client exports retain the public address.
Only use this inventory on that server. To move control elsewhere, remove `local: true` and update
key paths. Local setup handles one server; multi-host management is described below.

### Manage from another machine

```shell
./mega-proxy inventory
./mega-proxy bootstrap
Expand Down
53 changes: 53 additions & 0 deletions docs/ru/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,59 @@ ACME-сертификаты на публичный IP требуют Certbot 5.

## Первое развёртывание

### Установка на самом сервере

Подключитесь к Debian/Ubuntu по SSH с терминалом (`ssh -t`) либо откройте консоль сервера.
Нужны root или `sudo`, установленный `curl` и доступ в интернет. Если `curl` отсутствует:

```shell
sudo apt-get update && sudo apt-get install -y curl ca-certificates
```

Запустите мастер (для root уберите `sudo`):

```shell
curl -fsSL https://raw.githubusercontent.com/andre487/MegaProxyServer/main/install.sh | sudo bash
```

Если хотите сначала прочитать скрипт, скачайте его через `curl -fsSL URL -o install.sh`,
просмотрите и выполните `sudo bash install.sh`, подставив URL из команды выше.

1. Подтвердите установку зависимостей. Репозиторий сохраняется в `/opt/megaproxy-server`;
Python и зависимости проекта устанавливаются автоматически.
2. Укажите имя сервера, его **публичный** IP или DNS, административный логин и текущий SSH-порт.
Выберите или создайте административный ключ, затем HTTPS и/или SSH, сертификат и пользователей.
Для доменного ACME заранее направьте DNS на этот сервер. Откройте нужные порты у провайдера.
3. Задайте пароль Ansible Vault и сохраните его: он понадобится для следующих запусков.
4. Мастер покажет путь к административному приватному ключу. Скопируйте его на свой компьютер
через ещё открытый административный доступ. Для root-доступа можно использовать во втором
локальном терминале `scp root@SERVER:/ПУТЬ/К/КЛЮЧУ ./megaproxy-admin`, затем
`chmod 600 ./megaproxy-admin`. При входе через sudo-пользователя используйте защищённую передачу
с чтением файла через `sudo`; не делайте ключ общедоступным на сервере.
5. Только после сохранения ключа подтвердите применение. Мастер создаёт администратора локально,
проверяет независимый SSH-вход и sudo через loopback, отключает root/парольный административный
вход, настраивает сервисы и firewall, проверяет их и создаёт клиентские конфигурации.
6. Не закрывая исходную сессию, проверьте вход **со своего компьютера**:
`ssh -i ./megaproxy-admin -p PORT ADMIN@SERVER`. Заберите клиентский файл
`/opt/megaproxy-server/.generated/configs/MegaProxy.json` через защищённый канал с sudo.

Если остановились перед применением или установка завершилась ошибкой, повторите команду
установщика либо запустите:

```shell
sudo /opt/megaproxy-server/mega-proxy setup-local
```

Существующий inventory используется повторно; установщик не обновляет checkout автоматически.
Для дальнейшей работы выполняйте `sudo /opt/megaproxy-server/mega-proxy` с нужной командой.
Inventory, ключи и экспорты остаются на сервере; сохраняйте их резервные копии приватно.
В этом режиме `local: true` направляет управляющий SSH на loopback, а публичный адрес используется
в клиентских конфигурациях. Такой inventory предназначен для запуска только на этом сервере;
при переносе управления на другую машину уберите `local: true` и исправьте пути к ключам.
Мастер локальной установки настраивает один сервер; управление несколькими хостами описано ниже.

### Управление с другой машины

```shell
./mega-proxy inventory
./mega-proxy bootstrap
Expand Down
57 changes: 57 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# Keep execution after the complete function definition for curl | bash.
main() {
set -euo pipefail
umask 077
if [[ ${1:-} == --help ]]; then
echo 'Usage: curl -fsSL https://raw.githubusercontent.com/andre487/MegaProxyServer/main/install.sh | sudo bash'
echo 'Interactive Debian/Ubuntu installation into /opt/megaproxy-server. Requires a terminal.'
echo 'Set MEGAPROXY_REF to select a branch or tag for a fresh checkout (default: main).'
return
fi
if [[ $# -ne 0 ]]; then
echo 'Unexpected arguments; use --help.' >&2
return 2
fi
if [[ $EUID -ne 0 ]]; then
echo 'Run as root: curl -fsSL URL | sudo bash' >&2
return 1
fi
if [[ ! -r /etc/os-release ]]; then
echo 'Only Debian and Ubuntu are supported.' >&2
return 1
fi
. /etc/os-release
case "$ID" in
debian|ubuntu) ;;
*) echo 'Only Debian and Ubuntu are supported.' >&2; return 1 ;;
esac
# stdin contains the script when piped; all prompts must read the terminal.
if ! exec </dev/tty; then
echo 'An interactive terminal is required (connect with ssh -t).' >&2
return 1
fi
local install_dir=/opt/megaproxy-server answer
echo "Install dependencies and MegaProxyServer into $install_dir, then start the setup wizard."
read -r -p 'Continue? [y/N] ' answer
[[ "$answer" == [yY] ]] || return 0
apt-get update
DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates curl git python3 python3-venv openssh-server sudo
systemctl enable --now ssh
if [[ ! -e "$install_dir" ]]; then
git clone --depth 1 --branch "${MEGAPROXY_REF:-main}" https://github.com/andre487/MegaProxyServer.git "$install_dir"
elif [[ ! -f "$install_dir/src/megaproxy_server/local_setup.py" ]]; then
echo "$install_dir already exists without local setup support; move it aside or update it manually." >&2
return 1
fi
cd "$install_dir"
if ! command -v uv >/dev/null 2>&1; then
python3 -m venv .bootstrap
.bootstrap/bin/pip install 'uv==0.12.5'
ln -s "$install_dir/.bootstrap/bin/uv" /usr/local/bin/uv
fi
uv sync --frozen --python 3.12
./mega-proxy setup-local
}

main "$@"
12 changes: 9 additions & 3 deletions src/megaproxy_server/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

from ruamel.yaml import YAML

from .inventory import ROOT, ansible_inventory, save
from .inventory import LOCAL_SSH_ARGS, ROOT, ansible_inventory, save
from .models import Host, Inventory


Expand All @@ -20,10 +20,11 @@ def check_admin(host: Host, *, report_error: bool = False) -> bool:
'-o', 'PreferredAuthentications=publickey',
'-o', 'ControlMaster=no', '-o', 'ControlPath=none', '-o', 'ConnectTimeout=10',
'-p', str(host.admin.port), '-i', str(Path(host.admin.private_key_file).expanduser()),
*(shlex.split(LOCAL_SSH_ARGS) if host.local else []),
*shlex.split(os.environ.get('ANSIBLE_SSH_ARGS', '')),
*shlex.split(os.environ.get('ANSIBLE_SSH_COMMON_ARGS', '')),
*shlex.split(os.environ.get('ANSIBLE_SSH_EXTRA_ARGS', '')),
'-l', host.admin.user, '--', host.address, 'sudo -n true',
'-l', host.admin.user, '--', '127.0.0.1' if host.local else host.address, 'sudo -n true',
]
try:
result = sp.run(command, capture_output=True, text=True, timeout=30)
Expand Down Expand Up @@ -52,6 +53,11 @@ def run_phase(inventory: Inventory, name: str, phase: str, password: str | None
variables['ansible_become_password'] = password
variables['ansible_ssh_common_args'] = '-o PubkeyAuthentication=no -o PreferredAuthentications=password,keyboard-interactive'
variables['megaproxy_bootstrap_phase'] = phase
if host.local and phase == 'account':
if os.geteuid() != 0:
raise ValueError('Local bootstrap must run as root')
variables['ansible_connection'] = 'local'
variables['ansible_python_interpreter'] = sys.executable
# Bootstrap passwords exist only in a private temporary directory for this subprocess.
with tempfile.TemporaryDirectory(prefix='megaproxy-bootstrap-') as directory:
path = Path(directory) / 'inventory.yml'
Expand All @@ -73,7 +79,7 @@ def bootstrap(path: Path, inventory: Inventory, limit: str | None = None) -> int
print(f'Bootstrapping administrative access: {name}', flush=True)
if not check_admin(host):
password = None
if host.admin.bootstrap_auth == 'password':
if host.admin.bootstrap_auth == 'password' and not host.local:
password = getpass.getpass(f'Initial SSH password for {host.admin.bootstrap_user}@{host.address}: ')
if not password:
raise ValueError('Initial SSH password must not be empty')
Expand Down
4 changes: 4 additions & 0 deletions src/megaproxy_server/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def parser() -> argparse.ArgumentParser:
sub = result.add_subparsers(dest="command")
commands = []
commands.append(sub.add_parser("inventory", help="Create a new inventory"))
commands.append(sub.add_parser("setup-local", help="Interactively provision this server (root)"))
commands.append(sub.add_parser("add-host", help="Add hosts to the existing inventory"))
initial = sub.add_parser("bootstrap", help="Create and verify permanent administrative access")
initial.add_argument("--limit", action=ExtendLimit, help="Exact host names, comma-separated or repeated")
Expand Down Expand Up @@ -134,6 +135,9 @@ def main() -> None:
try:
if args.command == "check":
raise SystemExit(run_checks())
if args.command == "setup-local":
from .local_setup import setup_local
raise SystemExit(setup_local(discover(args.inventory) or DEFAULT_INVENTORY))
path = choose_inventory(args.inventory, args.command == "inventory")
if args.command == "inventory":
print(f"Created {path}")
Expand Down
9 changes: 8 additions & 1 deletion src/megaproxy_server/inventory.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import ipaddress
import os
import re
import shlex
from pathlib import Path
from typing import Any

Expand All @@ -15,6 +16,10 @@
from .models import Inventory

ROOT = Path(__file__).resolve().parents[2]
LOCAL_SSH_ARGS = (
"-o StrictHostKeyChecking=yes -o UserKnownHostsFile="
+ shlex.quote(str(ROOT / ".secrets/local-known-hosts"))
)
DEFAULT_INVENTORY = Path.cwd() / "inventory.yml"
POINTER_FILE = Path.cwd() / "inventory-path"
_vault_secret: VaultSecret | None = None
Expand Down Expand Up @@ -215,13 +220,15 @@ def ansible_inventory(inventory: Inventory) -> dict[str, Any]:
}
)
variables: dict[str, Any] = {
"ansible_host": host.address,
"ansible_host": "127.0.0.1" if host.local else host.address,
"ansible_user": host.admin.bootstrap_user or host.admin.user,
"ansible_port": host.admin.port,
"megaproxy_admin": host.admin.model_dump(mode="json", exclude_none=True),
"megaproxy_settings": inventory.settings.model_dump(mode="json"),
"megaproxy_services": services,
}
if host.local:
variables["ansible_ssh_common_args"] = LOCAL_SSH_ARGS
if https and https.enabled:
variables["megaproxy_https_public_routes"] = variables_public_routes
variables["ansible_ssh_private_key_file"] = (
Expand Down
52 changes: 52 additions & 0 deletions src/megaproxy_server/local_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import os
from pathlib import Path

import questionary

from .bootstrap import bootstrap
from .generate import generate
from .inventory import ROOT, load
from .wizard import create_inventory


def setup_local(path: Path) -> int:
from .cli import run_ansible

if os.geteuid() != 0:
raise ValueError('Local setup must run as root; use sudo')
os.umask(0o077)
inventory = load(path) if path.exists() else create_inventory(path, local=True)
if len(inventory.hosts) != 1 or not next(iter(inventory.hosts.values())).local:
raise ValueError('Local setup requires an inventory with exactly one local host')
host = next(iter(inventory.hosts.values()))
# Trust the host's own keys, without a network scan or disabling SSH verification.
keys = list(Path('/etc/ssh').glob('ssh_host_*_key.pub'))
if not keys:
raise ValueError('No SSH host keys found in /etc/ssh; start openssh-server first')
known_hosts = ROOT / '.secrets/local-known-hosts'
known_hosts.parent.mkdir(parents=True, exist_ok=True, mode=0o700)
known_hosts.write_text(''.join(
f'127.0.0.1,[127.0.0.1]:{host.admin.port} {key.read_text().strip()}\n'
for key in keys
))
known_hosts.chmod(0o600)
print(f'Inventory: {path}\nAdministrative private key: {host.admin.private_key_file}')
print('Copy this private key securely to your computer before continuing. Keep this session open.')
print(f'Future SSH login: {host.admin.user}@{host.address}, port {host.admin.port}')
print('Setup disables root/password administrative SSH login and configures the host firewall.')
if not questionary.confirm('Key saved outside this server; apply configuration now?', default=False).ask():
print('Configuration saved. Run setup-local again to continue.')
return 0
code = bootstrap(path, inventory)
if code:
return code
for playbook in ('site.yml', 'verify.yml'):
code = run_ansible(path, playbook)
if code:
return code
output = ROOT / '.generated/configs'
generate(path, output)
print(f'Setup verified. Client configurations (contain secrets): {output}')
return 0
1 change: 1 addition & 0 deletions src/megaproxy_server/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def any_enabled(self) -> Services:

class Host(BaseModel):
address: str
local: bool = False
admin: AdminAccess = Field(default_factory=AdminAccess)
services: Services

Expand Down
Loading
Loading