11 Commits

28 changed files with 467 additions and 2352 deletions

View File

@@ -0,0 +1,41 @@
solana_user: solana
solana_group: solana
solana_home: /var/lib/solana
solana_install_script_url: https://release.anza.xyz/stable/install
solana_active_release_bin_dir: "{{ solana_home }}/.local/share/solana/install/active_release/bin"
solana_validator_bin: /opt/solana/bin/agave-validator
solana_keygen_bin: /opt/solana/bin/solana-keygen
solana_rpc_service_name: solana-rpc
agave_repo_url: https://github.com/anza-xyz/agave.git
agave_version_tag: v2.3.13
agave_src_dir: "{{ solana_home }}/src/agave"
agave_rust_toolchain: "1.86.0"
solana_identity_path: /var/lib/solana/identity.json
solana_ledger_dir: /var/lib/solana/ledger
solana_accounts_dir: /var/lib/solana/accounts
solana_log_dir: /var/log/solana
# Note: agave-validator expects all sockets to be bound to the same IP.
# For now we bind validator + RPC to 0.0.0.0 and rely on network hardening in a later etap.
solana_bind_address: 0.0.0.0
solana_rpc_port: 8899
solana_rpc_pubsub_port: 8900
solana_dynamic_port_range: "8000-8020"
solana_entrypoints:
- entrypoint.mainnet-beta.solana.com:8001
solana_known_validators: []
solana_geyser_enabled: false
solana_geyser_plugin_config_path: /etc/solana/yellowstone-geyser.json
solana_rpc_extra_args:
- --full-rpc-api
- --limit-ledger-size
solana_rpc_manage_service: true
solana_rpc_enable_on_boot: true
solana_rpc_start_now: true

View File

@@ -1,2 +1,2 @@
[sol_rpc]
mevnode ansible_host=n8c1e71.mevnode.com ansible_user=root
mevnode ansible_host=n8c1e71.mevnode.com ansible_user=root ansible_ssh_private_key_file=/home/runner/.ssh/mevnode_baremetal

View File

@@ -3,6 +3,8 @@
hosts: sol_rpc
gather_facts: true
become: true
vars_files:
- ../group_vars/sol_rpc.yml
tasks:
- name: Install operator packages (Debian/Ubuntu)
@@ -16,6 +18,32 @@
update_cache: true
when: ansible_facts.os_family == "Debian"
- name: Install Solana host base packages (Debian/Ubuntu)
ansible.builtin.apt:
name:
- chrony
- curl
- jq
- smartmontools
- nvme-cli
- prometheus-node-exporter
state: present
update_cache: true
when: ansible_facts.os_family == "Debian"
- name: Ensure solana group exists
ansible.builtin.group:
name: "{{ solana_group }}"
system: true
- name: Ensure solana user exists
ansible.builtin.user:
name: "{{ solana_user }}"
group: "{{ solana_group }}"
home: "{{ solana_home }}"
system: true
create_home: true
- name: Ensure root config directories exist
ansible.builtin.file:
path: "{{ item }}"
@@ -29,6 +57,38 @@
- /root/.config/nvim
- /root/.config/nvim/lua
- name: Ensure Solana directories exist
ansible.builtin.file:
path: "{{ item.path }}"
state: directory
owner: "{{ item.owner }}"
group: "{{ item.group }}"
mode: "{{ item.mode }}"
loop:
- { path: "/etc/solana", owner: "root", group: "root", mode: "0755" }
- { path: "{{ solana_home }}", owner: "{{ solana_user }}", group: "{{ solana_group }}", mode: "0750" }
- { path: "{{ solana_ledger_dir }}", owner: "{{ solana_user }}", group: "{{ solana_group }}", mode: "0750" }
- { path: "{{ solana_accounts_dir }}", owner: "{{ solana_user }}", group: "{{ solana_group }}", mode: "0750" }
- { path: "{{ solana_log_dir }}", owner: "{{ solana_user }}", group: "{{ solana_group }}", mode: "0750" }
- { path: "/opt/solana/bin", owner: "root", group: "root", mode: "0755" }
- name: Deploy Solana sysctl tuning (network buffers)
ansible.builtin.copy:
dest: /etc/sysctl.d/99-solana-rpc.conf
owner: root
group: root
mode: "0644"
content: |
# Required by agave-validator startup checks
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
register: solana_sysctl_conf
- name: Apply sysctl tuning when changed
ansible.builtin.command: sysctl --system
changed_when: true
when: solana_sysctl_conf.changed
- name: Deploy tmux config (Ctrl+a prefix)
ansible.builtin.copy:
src: ../files/operator-dotfiles/tmux.conf
@@ -64,6 +124,197 @@
- { src: "lua/utils.lua", dest: "lua/utils.lua" }
- { src: "lua/hazard3_dap.lua", dest: "lua/hazard3_dap.lua" }
- name: Deploy solana-rpc systemd unit (runs as solana user)
ansible.builtin.template:
src: ../templates/solana-rpc.service.j2
dest: /etc/systemd/system/{{ solana_rpc_service_name }}.service
owner: root
group: root
mode: "0644"
register: solana_rpc_unit
- name: Reload systemd after unit change
ansible.builtin.systemd:
daemon_reload: true
when: solana_rpc_unit.changed
- name: Check validator binary exists
ansible.builtin.stat:
path: "{{ solana_validator_bin }}"
register: solana_validator_bin_stat
- name: Install Agave toolchain for solana user when validator missing
ansible.builtin.shell: |
set -euo pipefail
sh -c "$(curl -sSfL {{ solana_install_script_url }})"
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
args:
executable: /bin/bash
when: not solana_validator_bin_stat.stat.exists
- name: Link Agave binaries into /opt/solana/bin
ansible.builtin.shell: |
set -euo pipefail
if [ ! -d "{{ solana_active_release_bin_dir }}" ]; then
echo "Active release bin dir missing: {{ solana_active_release_bin_dir }}" >&2
exit 1
fi
for bin in "{{ solana_active_release_bin_dir }}"/*; do
name="$(basename "$bin")"
ln -sfn "$bin" "/opt/solana/bin/$name"
done
args:
executable: /bin/bash
when: not solana_validator_bin_stat.stat.exists
- name: Re-check validator binary after install
ansible.builtin.stat:
path: "{{ solana_validator_bin }}"
register: solana_validator_bin_stat
- name: Install Agave build dependencies (Debian/Ubuntu) when validator missing
ansible.builtin.apt:
name:
- build-essential
- pkg-config
- libssl-dev
- clang
- llvm-dev
- libclang-dev
- cmake
- protobuf-compiler
- libudev-dev
- zlib1g-dev
state: present
update_cache: true
when:
- ansible_facts.os_family == "Debian"
- not solana_validator_bin_stat.stat.exists
- name: Ensure Agave source parent directory exists
ansible.builtin.file:
path: "{{ agave_src_dir | dirname }}"
state: directory
owner: "{{ solana_user }}"
group: "{{ solana_group }}"
mode: "0755"
when: not solana_validator_bin_stat.stat.exists
- name: Install rustup (Agave build) for solana user when validator missing
ansible.builtin.shell: |
set -euo pipefail
curl -sSfL https://sh.rustup.rs | sh -s -- -y --default-toolchain "{{ agave_rust_toolchain }}"
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
args:
executable: /bin/bash
creates: "{{ solana_home }}/.cargo/bin/cargo"
when: not solana_validator_bin_stat.stat.exists
- name: Ensure Rust toolchain is installed for solana user when validator missing
ansible.builtin.shell: |
set -euo pipefail
export PATH="{{ solana_home }}/.cargo/bin:$PATH"
rustup toolchain install "{{ agave_rust_toolchain }}"
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
args:
executable: /bin/bash
when: not solana_validator_bin_stat.stat.exists
- name: Clone Agave sources when validator missing
ansible.builtin.git:
repo: "{{ agave_repo_url }}"
dest: "{{ agave_src_dir }}"
version: "{{ agave_version_tag }}"
depth: 1
update: true
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
when: not solana_validator_bin_stat.stat.exists
- name: Build agave-validator from sources when validator missing
ansible.builtin.shell: |
set -euo pipefail
export PATH="{{ solana_home }}/.cargo/bin:$PATH"
cd "{{ agave_src_dir }}"
cargo +{{ agave_rust_toolchain }} build --release -p agave-validator
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
args:
executable: /bin/bash
creates: "{{ agave_src_dir }}/target/release/agave-validator"
when: not solana_validator_bin_stat.stat.exists
- name: Install agave-validator to /opt/solana/bin when built
ansible.builtin.copy:
remote_src: true
src: "{{ agave_src_dir }}/target/release/agave-validator"
dest: "{{ solana_validator_bin }}"
owner: root
group: root
mode: "0755"
when: not solana_validator_bin_stat.stat.exists
- name: Re-check validator binary after source build
ansible.builtin.stat:
path: "{{ solana_validator_bin }}"
register: solana_validator_bin_stat
- name: Ensure identity key exists
ansible.builtin.shell: |
set -euo pipefail
"{{ solana_keygen_bin }}" new --no-passphrase -o "{{ solana_identity_path }}"
become_user: "{{ solana_user }}"
environment:
HOME: "{{ solana_home }}"
args:
executable: /bin/bash
creates: "{{ solana_identity_path }}"
- name: Check identity key exists
ansible.builtin.stat:
path: "{{ solana_identity_path }}"
register: solana_identity_stat
- name: Ensure solana-rpc service state when prerequisites exist
ansible.builtin.systemd:
name: "{{ solana_rpc_service_name }}"
enabled: "{{ solana_rpc_enable_on_boot | bool }}"
state: "{{ 'started' if (solana_rpc_start_now | bool) else 'stopped' }}"
when:
- solana_rpc_manage_service | bool
- solana_validator_bin_stat.stat.exists
- solana_identity_stat.stat.exists
- name: Verify solana-rpc is active
ansible.builtin.command: systemctl is-active {{ solana_rpc_service_name }}
register: solana_rpc_is_active
changed_when: false
retries: 10
delay: 3
until: solana_rpc_is_active.stdout.strip() == "active"
when:
- solana_rpc_manage_service | bool
- solana_validator_bin_stat.stat.exists
- solana_identity_stat.stat.exists
- name: Report skipped solana-rpc start due to missing prerequisites
ansible.builtin.debug:
msg:
- "solana-rpc start skipped: missing prerequisites"
- "validator_bin={{ solana_validator_bin }} exists={{ solana_validator_bin_stat.stat.exists }}"
- "identity={{ solana_identity_path }} exists={{ solana_identity_stat.stat.exists }}"
when:
- solana_rpc_manage_service | bool
- not (solana_validator_bin_stat.stat.exists and solana_identity_stat.stat.exists)
- name: Validate Ansible transport
ansible.builtin.ping:

View File

@@ -0,0 +1,35 @@
[Unit]
Description=Solana RPC node (Agave)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User={{ solana_user }}
Group={{ solana_group }}
WorkingDirectory={{ solana_home }}
Environment=RUST_LOG=info
LimitNOFILE=1048576
Restart=always
RestartSec=5
TimeoutStopSec=120
ExecStart={{ solana_validator_bin }} \
--identity {{ solana_identity_path }} \
--ledger {{ solana_ledger_dir }} \
--accounts {{ solana_accounts_dir }} \
--bind-address {{ solana_bind_address }} \
--rpc-bind-address {{ solana_bind_address }} \
--rpc-port {{ solana_rpc_port }} \
--dynamic-port-range {{ solana_dynamic_port_range }}{% for ep in solana_entrypoints %} \
--entrypoint {{ ep }}{% endfor %}{% for kv in solana_known_validators %} \
--known-validator {{ kv }}{% endfor %}{% if solana_geyser_enabled | bool %} \
--geyser-plugin-config {{ solana_geyser_plugin_config_path }}{% endif %}{% for arg in solana_rpc_extra_args %} \
{{ arg }}{% endfor %} \
--log {{ solana_log_dir }}/validator.log
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths={{ solana_ledger_dir }} {{ solana_accounts_dir }} {{ solana_log_dir }} /var/lib/solana
[Install]
WantedBy=multi-user.target

View File

@@ -4,7 +4,7 @@ Cel etapu: uruchomić pierwszy, bezpieczny playbook Ansible dla hosta RPC (`mevn
## Zakres
1. Sprawdzić i zainstalować Ansible na VPS (control node), jeśli brak.
1. Uruchamiać Ansible na VPS jako runner w Dockerze (bez instalacji Ansible na hoście), jeśli to jest preferowany model operacyjny.
2. Dodać minimalną strukturę Ansible w `trade-iac`:
- inventory dla `mevnode`,
- minimalny playbook testowy (bez zmian destrukcyjnych).
@@ -18,6 +18,23 @@ Cel etapu: uruchomić pierwszy, bezpieczny playbook Ansible dla hosta RPC (`mevn
## Kryteria akceptacji
- `ansible-playbook --version` działa na VPS.
- `ansible-playbook --version` działa w kontenerze na VPS.
- Playbook kończy się statusem success dla grupy `sol_rpc`.
- Wynik zawiera podstawowe fakty hosta i potwierdzenie łączności Ansible.
## Jak uruchomić na VPS (Docker)
Przykład (image: `quay.io/ansible/ansible-runner:latest`):
```bash
cd /opt/trade-iac
docker run --rm -t \
-v "$PWD/ansible:/ansible" \
-v "$HOME/.ssh:/home/runner/.ssh:ro" \
-w /ansible \
quay.io/ansible/ansible-runner:latest \
ansible-playbook -i inventory/hosts.ini playbooks/doc-rpc-sol-min.yml
```
Ważne: inventory w `ansible/inventory/hosts.ini` używa jawnie pliku klucza
`/home/runner/.ssh/mevnode_baremetal` (czyli wymaga montowania `~/.ssh` do `/home/runner/.ssh` w kontenerze).

View File

@@ -0,0 +1,22 @@
# Etap 005: Solana RPC jako usługa użytkownika `solana`
Cel etapu: przygotować i wdrożyć baseline pod `solana-rpc`, uruchamiany jako dedykowany użytkownik systemowy `solana` (nie `root`).
## Zakres
1. Rozszerzyć playbook o:
- pakiety bazowe dla hosta RPC,
- utworzenie użytkownika i katalogów `solana`,
- deployment unitu `systemd` `solana-rpc.service` z `User=solana`.
2. Dodać zmienne grupowe `sol_rpc` (ścieżki, porty, opcje startu).
3. Dodać bezpieczną logikę startu:
- unit jest wdrażany zawsze,
- start usługi tylko gdy istnieją wymagane artefakty (`agave-validator`, identity keypair).
4. Wdrożyć na `mevnode` i zweryfikować stan.
## Kryteria akceptacji
- `id solana` istnieje na `mevnode`.
- `/etc/systemd/system/solana-rpc.service` istnieje i zawiera `User=solana`.
- Playbook kończy się bez błędów.
- Jeśli binarka/identity nie istnieją, playbook raportuje to jawnie i nie wymusza startu.

View File

@@ -0,0 +1,31 @@
# Etap 006: Instalacja Agave + identity + start `solana-rpc`
Cel etapu: domknąć bootstrap uruchomienia `solana-rpc` jako `solana` przez:
1. instalację binarki `agave-validator`,
2. wygenerowanie `identity.json` (jeśli brak),
3. start usługi `solana-rpc` i test endpointu RPC.
## Zakres
- Rozszerzyć playbook o zadania instalacyjne Agave (idempotentnie).
- Dodać provisioning `identity` jako użytkownik `solana`.
- Dodać minimalny tuning OS wymagany przez startup check (`sysctl`).
- Utrzymać bezpieczny start: usługa uruchamiana tylko przy komplecie prereq.
- Wykonać testy powdrożeniowe (`systemd`, port, JSON-RPC).
## Założenia
- W tej wersji `agave-validator` wszystkie sockety (gossip/TPU/RPC) muszą być zbindowane do tego samego IP.
- Na czas bootstrapu bind jest na `0.0.0.0` (żeby validator przeszedł check reachability i wystartował).
- Produkcyjny bind na WG IP i hardening sieciowy będzie osobnym etapem.
- Release tar z `agave-install` nie zawiera `agave-validator`, więc `agave-validator` budujemy ze źródeł (tag `v2.x`) i instalujemy do `/opt/solana/bin`.
- Build wymaga pakietów dev, w tym `libclang`/`llvm` (Ansible instaluje je w playbooku).
## Kryteria akceptacji
- `agave-validator` istnieje pod `/opt/solana/bin/agave-validator`.
- `identity` istnieje pod `/var/lib/solana/identity.json` (owner `solana`).
- `systemctl is-active solana-rpc` zwraca `active`.
- Endpoint `http://127.0.0.1:8899` odpowiada na JSON-RPC.
- Websocket (pubsub) jest na porcie `solana_rpc_port + 1` (czyli domyślnie `8900` przy `8899`).

View File

@@ -1,68 +0,0 @@
Step 001: Auto-dodawanie klucza SSH do ssh-agent po restarcie (VPS -> baremetal)
Cel:
- Po restarcie VPS (bez recznego ssh-add) klucz prywatny do baremetala ma byc automatycznie dodany do ssh-agent.
- To jest wygodne, gdy po WG chcesz robic deploy/Ansible/ssh bez interakcji.
Wymagania:
- VPS z systemd (Debian/Ubuntu).
- Klucz prywatny jest na VPS, np. ~/.ssh/mevnode_baremetal
- Klucz NIE powinien miec passphrase (inaczej ssh-add bedzie chcial hasla i usluga oneshot padnie).
0) Plik klucza (prawa)
chmod 600 ~/.ssh/mevnode_baremetal
1) Wlacz "linger" dla usera (zeby user services startowaly po boot bez logowania)
sudo loginctl enable-linger user
2) Utworz usluge ssh-agent (systemd --user)
mkdir -p ~/.config/systemd/user
cat >~/.config/systemd/user/ssh-agent.service <<'EOF'
[Unit]
Description=SSH agent
[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a $SSH_AUTH_SOCK
[Install]
WantedBy=default.target
EOF
3) Utworz usluge ssh-add dla klucza do baremetala (systemd --user)
Uwaga: dodajemy ExecStartPre, zeby poczekac az socket agenta bedzie gotowy.
cat >~/.config/systemd/user/ssh-add-mevnode.service <<'EOF'
[Unit]
Description=ssh-add mevnode_baremetal
After=ssh-agent.service
Requires=ssh-agent.service
[Service]
Type=oneshot
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStartPre=/usr/bin/sh -c 'for i in $(seq 1 50); do [ -S "$SSH_AUTH_SOCK" ] && exit 0; sleep 0.1; done; echo "SSH_AUTH_SOCK not ready: $SSH_AUTH_SOCK" >&2; exit 1'
ExecStart=/usr/bin/ssh-add %h/.ssh/mevnode_baremetal
RemainAfterExit=yes
[Install]
WantedBy=default.target
EOF
4) Reload i wlacz autostart
systemctl --user daemon-reload
systemctl --user enable --now ssh-agent.service ssh-add-mevnode.service
5) Sprawdz dzialanie
systemctl --user status ssh-agent.service ssh-add-mevnode.service --no-pager
SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket" ssh-add -l
6) Test SSH na baremetal (po WG)
ssh user@10.66.66.1
Wskazowka:
- Alternatywa bez ssh-agent: wpis w ~/.ssh/config z IdentityFile (nie wymaga ssh-add),
ale ten plik opisuje wariant stricte "auto ssh-add po reboot".

View File

@@ -1,88 +0,0 @@
Step 001: Kolejnosc wdrozenia (najpierw WireGuard, potem odciecie publicznego SSH)
Cel: najpierw stawiamy WireGuard i testujemy polaczenie po WG, a dopiero potem ucinamy publiczne SSH.
To minimalizuje ryzyko zablokowania sie na baremetalu.
0) Pre-flight (nie zamykaj obecnej sesji SSH)
- Na baremetalu zostaw otwarta aktualna sesje SSH.
- Przygotuj drugi terminal do testow.
Sprawdz publiczny interfejs/IP routing:
ip route get 1.1.1.1
1) WireGuard na baremetalu (nie ruszaj SSH/firewall poza WG portem)
sudo apt-get update
sudo apt-get install -y wireguard
Jesli uzywasz skryptu:
sudo /usr/local/sbin/wg_serverctl.py gen-server --force
sudo /usr/local/sbin/wg_serverctl.py gen-client client1
sudo /usr/local/sbin/wg_serverctl.py add client1
sudo /usr/local/sbin/wg_serverctl.py export client1
Uruchom WG na serwerze:
sudo systemctl enable --now wg-quick@wg0
sudo wg show
ip a show wg0
2) WireGuard na kliencie i test polaczenia
Na kliencie:
sudo apt-get update
sudo apt-get install -y wireguard
sudo /usr/local/sbin/wg_client.py install ./client1.conf
sudo /usr/local/sbin/wg_client.py up
sudo /usr/local/sbin/wg_client.py status
Testy (z klienta):
ping 10.66.66.1
ssh user@10.66.66.1
Dopiero jak to dziala, idziesz dalej.
3) (Opcjonalnie teraz) UFW: wpusc WG, ale NIE odcinaj jeszcze publicznego SSH
Na baremetalu:
sudo apt-get install -y ufw
sudo ufw allow 51820/udp
sudo ufw allow 22/tcp (tymczasowo, zeby nic nie uciac przy wlaczaniu UFW)
sudo ufw enable
sudo ufw status verbose
Uwagi:
- Jesli `sudo ufw status verbose` pokazuje "Status: inactive", to reguly NIE dzialaja.
- W takim wypadku wlacz UFW bezpiecznie (zostawiajac dostep przez WG):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 51820/udp
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw deny in on eth0 to any port 22 proto tcp
sudo ufw enable
sudo ufw status verbose
4) Przestawienie SSH na "tylko WireGuard"
1) Najpierw pozwol na SSH po wg0:
sudo ufw allow in on wg0 to any port 22 proto tcp
2) Potem ogranicz sshd do WG IP:
sudo sshd -t
sudoedit /etc/ssh/sshd_config
Dodaj/ustaw:
PasswordAuthentication no
PermitRootLogin no
ListenAddress 10.66.66.1
Restart i test:
sudo systemctl restart ssh
Z klienta:
ssh user@10.66.66.1
3) Jak test przeszedl: zablokuj publiczne SSH:
sudo ufw deny 22/tcp
sudo ufw status verbose
5) RPC tylko po WG (po instalacji Agave)
Docelowo:
- bind RPC/WS na 10.66.66.1 (wg0)
- UFW: allow 8899/8900 tylko "in on wg0"

View File

@@ -1,25 +0,0 @@
Step 001: Jak skopiowac klucze SSH na uzytkownika "user" uruchamiajac komendy jako root
Cel: skopiowac klucze/konfiguracje SSH z /root/.ssh do /home/user/.ssh albo tylko dodac klucz publiczny do authorized_keys.
Wariant A: Skopiuj caly katalog /root/.ssh do usera (ustaw prawa)
sudo install -d -m 700 -o user -g user /home/user/.ssh
sudo cp -a /root/.ssh/. /home/user/.ssh/
sudo chown -R user:user /home/user/.ssh
sudo chmod 700 /home/user/.ssh
sudo chmod 600 /home/user/.ssh/authorized_keys 2>/dev/null || true
sudo chmod 600 /home/user/.ssh/id_* 2>/dev/null || true
sudo chmod 644 /home/user/.ssh/*.pub 2>/dev/null || true
sudo chmod 644 /home/user/.ssh/known_hosts 2>/dev/null || true
Uwaga: kopiowanie prywatnych kluczy (id_ed25519/id_rsa) na innego uzytkownika zwieksza ryzyko. Czasem lepiej uzyc wariantu B.
Wariant B: Tylko dopisz klucz publiczny roota do authorized_keys usera
sudo install -d -m 700 -o user -g user /home/user/.ssh
sudo install -m 600 -o user -g user /dev/null /home/user/.ssh/authorized_keys
sudo cat /root/.ssh/id_ed25519.pub | sudo tee -a /home/user/.ssh/authorized_keys >/dev/null
sudo chown user:user /home/user/.ssh/authorized_keys
sudo chmod 600 /home/user/.ssh/authorized_keys

View File

@@ -1,26 +0,0 @@
Step 001: Toggle SSH na publicznym interfejsie eth0 (ON/OFF) przez UFW
Zalozenie:
- SSH po WireGuard (wg0) zostaje zawsze wlaczone.
- Publiczne SSH po eth0 wlaczasz/wylaczasz tylko regula UFW.
- UFW musi byc aktywne (status != inactive).
1) Zawsze zostaw SSH po wg0:
sudo ufw allow in on wg0 to any port 22 proto tcp
2) ON: wlacz SSH po eth0 (public):
sudo ufw allow in on eth0 to any port 22 proto tcp
sudo ufw status verbose
3) OFF: wylacz SSH po eth0 (public):
sudo ufw deny in on eth0 to any port 22 proto tcp
sudo ufw status verbose
4) (Opcjonalnie) Usuniecie konkretnej reguly:
sudo ufw status numbered
sudo ufw delete <NUMER_REGULY_DLA_22_NA_eth0>
Uwaga:
- Jesli `sudo ufw status` pokazuje "inactive", to reguly nie dzialaja. Wtedy najpierw:
sudo ufw enable

View File

@@ -1,37 +0,0 @@
Step 001: Co zrobic z plikiem po "wg_serverctl.py export"
Po stronie serwera, po wykonaniu:
sudo /etc/wireguard/wg_serverctl.py export laptop1 --endpoint 149.50.116.219:51820
Powstaje plik:
/etc/wireguard/clients/laptop1.conf
Co dalej:
1) Skopiuj plik na klienta (laptop/VPS)
Najprosciej przez scp z klienta:
scp user@149.50.116.219:/etc/wireguard/clients/laptop1.conf .
2) Zainstaluj go jako /etc/wireguard/wg0.conf na kliencie
sudo install -m 600 laptop1.conf /etc/wireguard/wg0.conf
3) Podnies WireGuard na kliencie i przetestuj
sudo wg-quick up wg0
sudo wg show
ping 10.66.66.1
ssh user@10.66.66.1
4) Autostart po restarcie (server + client)
Domyslnie bywa, ze wg-quick@wg0 jest "disabled" (szczegolnie na kliencie).
Serwer (Ubuntu/Debian, systemd):
sudo systemctl enable --now wg-quick@wg0
sudo systemctl is-enabled wg-quick@wg0
sudo systemctl status wg-quick@wg0 --no-pager
Klient (Debian/Ubuntu, systemd):
sudo systemctl enable --now wg-quick@wg0
sudo systemctl is-enabled wg-quick@wg0
Wazne:
- Dopiero jak ping i ssh po WG dzialaja, przechodzisz do odciecia publicznego SSH na serwerze.

View File

@@ -1,116 +0,0 @@
# Step 001A: WireGuard tools (server/client) + instrukcje reset
Ten step dodaje dwa skrypty pomocnicze do zarzadzania WireGuard:
- server: `scripts/wireguard/wg_serverctl.py`
- client: `scripts/wireguard/wg_client.py`
Skrypty trzymaja stan na dysku, zgodnie z konwencja:
- server keys: `/etc/wireguard/server.key`, `/etc/wireguard/server.pub`
- clients: `/etc/wireguard/clients/<name>.key|.pub|.ip|.conf`
- generated server config: `/etc/wireguard/wg0.conf`
Wymagania: `wireguard-tools` (czyli `wg`, `wg-quick`) + `systemd`.
## Instalacja (server)
Na bare metalu:
```bash
apt-get update
apt-get install -y wireguard
```
Z repo (na hoście) zainstaluj skrypt jako root-only:
```bash
install -m 700 scripts/wireguard/wg_serverctl.py /usr/local/sbin/wg_serverctl.py
```
## Instalacja (client)
Na kliencie (laptop/VPS):
```bash
apt-get update
apt-get install -y wireguard
install -m 700 scripts/wireguard/wg_client.py /usr/local/sbin/wg_client.py
```
## Typowy flow (server)
```bash
# 1) wygeneruj klucze serwera
wg_serverctl.py gen-server --force
# 2) wygeneruj klucze klienta (trzymane na serwerze w /etc/wireguard/clients)
wg_serverctl.py gen-client client1
# 3) dodaj klienta do puli, wygeneruj wg0.conf i zrestartuj wg-quick@wg0
wg_serverctl.py add client1
# 4) wyeksportuj client config (do przekazania na klienta)
wg_serverctl.py export client1
# 5) podglad
wg_serverctl.py list
wg_serverctl.py show client1
```
Plik konfiguracyjny klienta powstaje jako:
- `/etc/wireguard/clients/client1.conf`
## Typowy flow (client)
Skopiuj `client1.conf` na klienta, a potem:
```bash
wg_client.py install ./client1.conf
wg_client.py up
wg_client.py status
```
## Stop / disable / reset wg0 (Ubuntu 24.04)
Stop (zostaw config, tylko down):
```bash
systemctl stop wg-quick@wg0
```
Disable autostart:
```bash
systemctl disable wg-quick@wg0
```
Flush (pewny down + usuniecie interfejsu):
```bash
systemctl stop wg-quick@wg0
ip link del wg0 2>/dev/null || true
ip a show wg0 2>/dev/null || echo "wg0 not present"
```
Hard reset (usuniecie kluczy + konfiguracji):
```bash
systemctl stop wg-quick@wg0
systemctl disable wg-quick@wg0
ip link del wg0 2>/dev/null || true
rm -f /etc/wireguard/wg0.conf
rm -f /etc/wireguard/server.key /etc/wireguard/server.pub
rm -rf /etc/wireguard/clients
```
Reset peerow bez kasowania server key:
```bash
systemctl stop wg-quick@wg0
rm -f /etc/wireguard/clients/*.pub /etc/wireguard/clients/*.ip
systemctl start wg-quick@wg0
```

View File

@@ -1,198 +0,0 @@
# Step 001: WireGuard + UFW + SSH tylko po WG (Ubuntu 24.04, 1 NIC)
Cel: na świeżym Ubuntu 24.04 na bare metalu ustawić:
- **WireGuard** jako prywatny kanał administracyjny,
- **SSH dostępne tylko przez WireGuard**,
- przygotować grunt pod **Agave RPC dostępne tylko przez WireGuard** (8899/8900 prywatnie),
- zostawić porty klastra Agave na publicznym NIC, żeby node mógł się zsynchronizować.
Kolejność jest krytyczna: najpierw uruchom WireGuard i sprawdź, że działa, dopiero potem ograniczaj SSH/firewall.
## Założenia
- Host ma **1 publiczny interfejs** (np. `enp1s0`).
- WireGuard interfejs to `wg0`.
- Masz dostęp awaryjny do konsoli (IPMI/KVM) na wypadek pomyłki.
## Parametry (dostosuj)
```bash
export PUBLIC_IFACE="enp1s0"
export WG_IFACE="wg0"
export WG_SERVER_IP="10.66.66.1/24"
export WG_SERVER_IP_ONLY="10.66.66.1"
export WG_PORT="51820"
export WG_CLIENT_IP="10.66.66.2/32"
export WG_CLIENT_PUBKEY="<PASTE_CLIENT_PUBLIC_KEY>"
# Agave/validator ports
export RPC_PORT="8899"
export WS_PORT="8900"
export DYNAMIC_PORT_RANGE="8000:8020"
```
## 1) WireGuard (nie ruszaj jeszcze SSH)
### Instalacja
```bash
apt-get update
apt-get install -y wireguard
```
### Klucze serwera
```bash
umask 077
wg genkey | tee /etc/wireguard/server.key >/dev/null
cat /etc/wireguard/server.key | wg pubkey | tee /etc/wireguard/server.pub >/dev/null
cat /etc/wireguard/server.pub
```
### Konfiguracja `/etc/wireguard/wg0.conf`
```bash
cat >/etc/wireguard/${WG_IFACE}.conf <<EOF
[Interface]
Address = ${WG_SERVER_IP}
ListenPort = ${WG_PORT}
PrivateKey = $(cat /etc/wireguard/server.key)
[Peer]
PublicKey = ${WG_CLIENT_PUBKEY}
AllowedIPs = ${WG_CLIENT_IP}
EOF
```
Start:
```bash
systemctl enable --now wg-quick@${WG_IFACE}
wg show
ip a show ${WG_IFACE}
```
Test z klienta (po zestawieniu tunelu): `ping 10.66.66.1`.
## 2) UFW: public tylko WG + porty klastra; prywatnie RPC/SSH po wg0
Instalacja:
```bash
apt-get install -y ufw
```
Public: pozwól na WireGuard:
```bash
ufw allow ${WG_PORT}/udp
```
Public: pozwól na porty klastra Agave (żeby node mógł syncować).
Uwaga: Solana/Agave używa głównie **UDP** w tym zakresie; jeśli będziesz miał problemy z reachability, rozważ dopuszczenie też TCP w tym samym zakresie.
```bash
ufw allow in on "${PUBLIC_IFACE}" to any port ${DYNAMIC_PORT_RANGE}/udp
```
Prywatnie (wg0): RPC/WS:
```bash
ufw allow in on "${WG_IFACE}" to any port ${RPC_PORT} proto tcp
ufw allow in on "${WG_IFACE}" to any port ${WS_PORT} proto tcp
```
Na tym etapie NIE blokuj jeszcze publicznego SSH (żeby się nie zablokować).
Włącz UFW:
```bash
ufw enable
ufw status verbose
```
## 3) SSH tylko po WireGuard (dopiero gdy WG działa)
### 3.1) UFW: pozwól na SSH po wg0
```bash
ufw allow in on "${WG_IFACE}" to any port 22 proto tcp
```
### 3.2) sshd: nasłuch tylko na WG IP
Edytuj `sshd_config`:
```bash
sed -n '1,220p' /etc/ssh/sshd_config
```
Dodaj (lub ustaw) na końcu pliku:
```conf
PasswordAuthentication no
PermitRootLogin no
ListenAddress 10.66.66.1
```
Zanim zrestartujesz SSH:
- miej otwartą obecną sesję SSH,
- zestaw WG na kliencie,
- przygotuj drugi terminal do testu po `10.66.66.1`.
Restart:
```bash
systemctl restart ssh
ss -lntp | grep -E ':22\\b' || true
```
Test z klienta po WG:
```bash
ssh <user>@10.66.66.1
```
Jeśli działa: zablokuj publiczny SSH:
```bash
ufw deny 22/tcp
ufw status verbose
```
## 4) Kernel/sysctl baseline pod Agave (na tym etapie bez uruchamiania Agave)
Minimalne wartości, które już mamy w Ansible (i kilka dodatkowych bezpiecznych):
```bash
cat >/etc/sysctl.d/90-agave.conf <<'EOF'
fs.file-max = 2000000
vm.max_map_count = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF
sysctl --system
```
Limity `nofile`:
```bash
cat >/etc/security/limits.d/90-solana.conf <<'EOF'
* soft nofile 1000000
* hard nofile 1000000
EOF
```
## 5) Co dalej
- Instalacja i uruchomienie Agave: patrz `doc/etap-007-manual-baremetal-rpc-commands.md`.
- Narzedzia do generowania konfiguracji WireGuard (server/client) + reset wg0: patrz `doc/step-001-wireguard-tools.md`.
- Docelowo: bind RPC/WS do WG IP (zamiast `0.0.0.0`) i spójne, minimalne reguły firewall.

View File

@@ -1,261 +0,0 @@
Step 002: Plan wdrozenia Solana RPC (Agave mainnet, non-voting, private RPC + Geyser->Postgres + monitoring + NOC tmux)
Zalozenia (default):
- WG iface: wg0, WG subnet: 10.66.66.0/24, WG IP baremetala: 10.66.66.1
- Public iface: eth0, public IP: 149.50.116.219
- RPC: 8899 (HTTP), WS: 8900 (WS = RPC_PORT+1)
- Postgres (lokalnie): 5432 (DB: geyser, user: geyser)
- (Optional) Yellowstone gRPC: 10000 (tylko po WG)
- Prometheus: 9090, Grafana: 3000, node_exporter: 9100 (tylko po WG)
- Dynamic port range (cluster): 8000-8020/udp (public eth0)
- User: solana
- Dirs:
- /var/lib/solana (ledger/accounts)
- /etc/solana (configs)
- /var/log/solana (logs)
0) Preconditions (check)
# WG dziala
ip -4 a show wg0
wg show
# SSH po WG dziala (z laptop/VPS):
# ssh user@10.66.66.1
1) Packages
sudo apt-get update
sudo apt-get install -y \
tmux btop \
sysstat iotop \
ethtool nstat \
chrony curl jq git ca-certificates \
ripgrep \
smartmontools nvme-cli \
build-essential pkg-config libssl-dev \
clang llvm-dev libclang-dev \
cmake protobuf-compiler libudev-dev zlib1g-dev
2) Users/Dirs
sudo getent group solana >/dev/null || sudo groupadd --system solana
sudo id -u solana >/dev/null 2>&1 || sudo useradd --system --gid solana \
--home-dir /var/lib/solana --create-home --shell /bin/bash solana
sudo install -d -o root -g root -m 0755 /etc/solana /opt/solana/bin
sudo install -d -o solana -g solana -m 0750 \
/var/lib/solana /var/lib/solana/ledger /var/lib/solana/accounts /var/log/solana
# identity permissions (po utworzeniu pliku):
# sudo chown solana:solana /var/lib/solana/identity.json
# sudo chmod 600 /var/lib/solana/identity.json
2A) Storage (rekomendowane)
# Variant A: jesli /dev/nvme0n1 jest wolny i chcesz go podzielic 50/50:
# doc/step-002a-storage-nvme0n1-xfs.txt
#
# Variant B (mpabi): masz juz 2x NVMe z XFS:
# - SOLACCT na accounts (nvme0n1p1)
# - SOLLEDG na ledger (nvme1n1p1)
# doc/step-002a-storage-existing-solacct-solledg-xfs.txt
3) Firewall/WG binding (private-only)
# UFW: deny everything inbound by default
sudo ufw default deny incoming
sudo ufw default allow outgoing
# WireGuard (public)
sudo ufw allow 51820/udp
# SSH only via WG
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw deny in on eth0 to any port 22 proto tcp
# RPC/WS only via WG
sudo ufw allow in on wg0 to any port 8899 proto tcp
sudo ufw allow in on wg0 to any port 8900 proto tcp
sudo ufw deny in on eth0 to any port 8899 proto tcp
sudo ufw deny in on eth0 to any port 8900 proto tcp
# Yellowstone gRPC only via WG
sudo ufw allow in on wg0 to any port 10000 proto tcp
sudo ufw deny in on eth0 to any port 10000 proto tcp
# Monitoring only via WG
sudo ufw allow in on wg0 to any port 9100 proto tcp
sudo ufw allow in on wg0 to any port 9090 proto tcp
sudo ufw allow in on wg0 to any port 3000 proto tcp
sudo ufw deny in on eth0 to any port 9100 proto tcp
sudo ufw deny in on eth0 to any port 9090 proto tcp
sudo ufw deny in on eth0 to any port 3000 proto tcp
# Cluster ports (public, inbound) for sync (UDP)
sudo ufw allow in on eth0 to any port 8000:8020 proto udp
# (opcjonalnie) jesli bedziesz mial problemy z reachability:
# sudo ufw allow in on eth0 to any port 8000:8020 proto tcp
sudo ufw enable
sudo ufw status verbose
4) Validator (Agave) config + systemd (non-voting RPC-only)
# sysctl baseline (Agave startup checks)
sudo tee /etc/sysctl.d/90-agave.conf >/dev/null <<'EOF'
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF
sudo sysctl --system
# ulimit baseline
sudo tee /etc/security/limits.d/90-solana.conf >/dev/null <<'EOF'
* soft nofile 1000000
* hard nofile 1000000
EOF
# Agave tools (solana-keygen etc.) for solana user
sudo -u solana -H bash -lc 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"'
# Copy binaries to /opt/solana/bin (avoid symlink permission gotchas)
sudo cp -a /var/lib/solana/.local/share/solana/install/active_release/bin/. /opt/solana/bin/
sudo find /opt/solana/bin -maxdepth 1 -type f -exec chmod 0755 {} \;
/opt/solana/bin/solana --version || true
/opt/solana/bin/solana-keygen --version
# Build agave-validator (pin tag + toolchain)
# Uwaga: dla nowszych stable release'ow binarka moze nie byc publikowana,
# wiec build ze zrodel jest wymagany.
# doc/step-002f-agave-build-from-source-latest.txt
# Identity (create once)
if [ ! -f /var/lib/solana/identity.json ]; then
sudo -u solana -H /opt/solana/bin/solana-keygen new --no-passphrase -o /var/lib/solana/identity.json
sudo chown solana:solana /var/lib/solana/identity.json
sudo chmod 600 /var/lib/solana/identity.json
fi
# Genesis hash (mainnet)
GENESIS_HASH="$(curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getGenesisHash"}' \
https://api.mainnet-beta.solana.com | jq -r .result)"
echo "$GENESIS_HASH"
# Oczekiwane (mainnet): 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d
# systemd unit (bind RPC to WG/localhost only)
# UWAGA: w unit file NIE uzywaj `\\` na koncu linii w ExecStart.
# Jesli wpiszesz `\\`, agave-validator dostanie literalny argument `\` i padnie z:
# "error: The subcommand '\\' wasn't recognized"
#
# Najprosciej: ExecStart w 1 linii, bez "kontynuacji" backslashami.
# Ale: zeby uniknac problemow z wklejaniem/lamaniem linii, ponizej jest bezpieczna wersja wielolinijkowa.
sudo tee /etc/systemd/system/agave-validator.service >/dev/null <<'EOF'
[Unit]
Description=Agave validator (mainnet, RPC-only, non-voting)
After=network-online.target wg-quick@wg0.service
Wants=network-online.target wg-quick@wg0.service
[Service]
Type=simple
User=solana
Group=solana
WorkingDirectory=/var/lib/solana
Environment=RUST_LOG=info
LimitNOFILE=1048576
Restart=always
RestartSec=5
TimeoutStopSec=120
ExecStart=/opt/solana/bin/agave-validator \
--identity /var/lib/solana/identity.json \
--no-voting \
--private-rpc \
--ledger /var/lib/solana/ledger \
--accounts /var/lib/solana/accounts \
--log /var/log/solana/validator.log \
--bind-address 0.0.0.0 \
--rpc-bind-address 10.66.66.1 \
--rpc-port 8899 \
--dynamic-port-range 8000-8020 \
--entrypoint entrypoint.mainnet-beta.solana.com:8001 \
--expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \
--full-rpc-api \
--limit-ledger-size 50000000
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/solana/ledger /var/lib/solana/accounts /var/log/solana /var/lib/solana
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now agave-validator
systemctl is-active agave-validator
journalctl -u agave-validator -n 80 --no-pager
# Jesli padnie na genesis (brak genesis.tar.bz2):
# doc/step-002c-genesis-download-mainnet.txt
#
# Jesli padnie na snapshot (mainnet wymaga snapshot):
# doc/step-002d-snapshot-download-mainnet.txt
#
# Jesli chcesz najpierw odpalic validator z CLI (test), zanim przepniesz na systemd:
# doc/step-002e-agave-validator-run-cli.txt
5) Geyser plugin -> Postgres
# Krok po kroku:
# doc/step-003-geyser-plugin-postgres.txt
#
# W skrocie:
# - budujesz plugin .so i wrzucasz do /opt/solana/plugins/
# - tworzysz /etc/solana/geyser-postgres.json
# - dopisujesz do ExecStart:
# --geyser-plugin-config /etc/solana/geyser-postgres.json
6) Drift DLOB (2 markets) (on VPS, not baremetal) (placeholder)
# VPS env:
# PERP_MARKETS_TO_LOAD=0,75
# USE_GRPC=true
# GRPC_ENDPOINT=http://10.66.66.1:10000
7) Monitoring (bind to WG only) (placeholder)
# node_exporter (prefer bind to WG):
# sudo apt-get install -y prometheus-node-exporter
# sudo systemctl enable --now prometheus-node-exporter
#
# Prometheus/Grafana (bind to 10.66.66.1):
# sudo apt-get install -y prometheus grafana
# sudo systemctl enable --now prometheus grafana-server
8) NOC tmux (btop) (placeholder)
# Create tmux session:
# btop
# iostat -x 1
# vmstat 1
# nstat -az
# ethtool -S eth0 | head -n 40 (wrap in watch)
# journalctl -fu agave-validator
# while true; do curl getHealth/getSlot; sleep 1; done
9) Troubleshooting checklist
# Services:
systemctl status agave-validator --no-pager
journalctl -u agave-validator -n 200 --no-pager
# Sockets and binds:
ss -lntp | egrep ':22\\b|:8899\\b|:8900\\b|:10000\\b|:9100\\b|:9090\\b|:3000\\b' || true
# Firewall:
sudo ufw status verbose
# RPC local:
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getHealth"}' \
http://127.0.0.1:8899 | jq .
5-min quick verification checklist
wg show
sudo ufw status verbose
systemctl is-active agave-validator
ss -lntp | egrep ':8899\\b|:8900\\b|:10000\\b|:9090\\b|:3000\\b|:9100\\b' || true
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getSlot"}' \
http://10.66.66.1:8899 | jq .

View File

@@ -1,214 +0,0 @@
Step 002: Instalacja Solana RPC (Agave) na baremetalu (po WireGuard)
Stan startowy:
- Masz dzialajacy WireGuard: z laptopa/VPS pingujesz 10.66.66.1 i logujesz sie: `ssh user@10.66.66.1`
- SSH po publicznym eth0 jest odciete (UFW deny na eth0:22), SSH dziala tylko po wg0.
- RPC ma byc prywatny: porty 8899/8900 tylko po wg0, ale node ma miec publiczne porty klastra (UDP dynamic range) zeby syncowal.
UWAGA o prywatnosci:
- RPC/WS bindujemy na wg0 (10.66.66.1), zeby NIE wystawic RPC na publicznym eth0 nawet gdy UFW jest wylaczone.
- Dodatkowo i tak robimy UFW "deny" na eth0:8899/8900 (defense-in-depth).
Parametry (dostosuj, przyklad mainnet):
- PUBLIC_IFACE=eth0
- WG_IFACE=wg0
- WG_IP=10.66.66.1
- ENDPOINT_RPC_PORT=8899
- ENDPOINT_WS_PORT=8900 (WS = RPC_PORT+1)
- DYNAMIC_PORT_RANGE="8000:8020"
0) Storage (rekomendowane)
Jesli masz osobny NVMe na dane (ledger/accounts), zrob najpierw:
doc/step-002a-storage-nvme0n1-xfs.txt
albo (mpabi, 2x NVMe z juz istniejacym XFS):
doc/step-002a-storage-existing-solacct-solledg-xfs.txt
1) Bazowe pakiety
sudo apt-get update
sudo apt-get install -y \
chrony curl jq git \
ripgrep \
smartmontools nvme-cli \
build-essential pkg-config libssl-dev \
clang llvm-dev libclang-dev \
cmake protobuf-compiler libudev-dev zlib1g-dev
2) Uzytkownik i katalogi
sudo getent group solana >/dev/null || sudo groupadd --system solana
sudo id -u solana >/dev/null 2>&1 || sudo useradd --system --gid solana \
--home-dir /var/lib/solana --create-home --shell /bin/bash solana
sudo install -d -o root -g root -m 0755 /etc/solana /opt/solana/bin
sudo install -d -o solana -g solana -m 0750 \
/var/lib/solana /var/lib/solana/ledger /var/lib/solana/accounts /var/log/solana
# Jesli ledger/accounts sa mountpointami (Step 002A), upewnij sie, ze root FS jest writable dla usera solana:
sudo chown solana:solana /var/lib/solana/ledger /var/lib/solana/accounts
sudo chmod 750 /var/lib/solana/ledger /var/lib/solana/accounts
3) Sysctl + ulimit (baseline pod Agave)
sudo tee /etc/sysctl.d/90-agave.conf >/dev/null <<'EOF'
fs.file-max = 2000000
vm.max_map_count = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF
sudo sysctl --system
sudo tee /etc/security/limits.d/90-solana.conf >/dev/null <<'EOF'
* soft nofile 1000000
* hard nofile 1000000
EOF
4) Firewall (UFW): RPC prywatnie po wg0, porty klastra publicznie po eth0
sudo ufw default deny incoming
sudo ufw default allow outgoing
# WireGuard z internetu
sudo ufw allow 51820/udp
# SSH tylko po wg0 (zakladamy, ze tak chcesz)
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw deny in on eth0 to any port 22 proto tcp
# RPC/WS tylko po wg0
sudo ufw allow in on wg0 to any port 8899 proto tcp
sudo ufw allow in on wg0 to any port 8900 proto tcp
sudo ufw deny in on eth0 to any port 8899 proto tcp
sudo ufw deny in on eth0 to any port 8900 proto tcp
# Porty klastra (inbound) na eth0 - UDP
sudo ufw allow in on eth0 to any port 8000:8020 proto udp
# (Opcjonalnie) jesli bedziesz mial problemy z reachability:
# sudo ufw allow in on eth0 to any port 8000:8020 proto tcp
sudo ufw enable
sudo ufw status verbose
5) Narzedzia Solana/Anza (dla `solana`): solana-keygen itd.
sudo -u solana -H bash -lc 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"'
# Weryfikacja, ze installer utworzyl active_release/bin (bez exit, zeby nie ubijac pane/tmux):
sudo -u solana -H bash -lc 'ls -ld ~/.local/share/solana/install/active_release/bin && ls -1 ~/.local/share/solana/install/active_release/bin | head'
# Kopiuj binarki do /opt/solana/bin (zamiast symlinkow - mniej problemow z uprawnieniami)
sudo cp -a /var/lib/solana/.local/share/solana/install/active_release/bin/. /opt/solana/bin/
sudo find /opt/solana/bin -maxdepth 1 -type f -exec chmod 0755 {} \;
/opt/solana/bin/solana-keygen --version
6) Build `agave-validator` ze zrodel i instalacja do /opt/solana/bin
# Najnowszy stable release dla mainnet wybieraj z GitHub Releases (tag v3.0.x).
# Krok po kroku (w tym automatyczny wybor tagu):
doc/step-002f-agave-build-from-source-latest.txt
7) Identity (tylko raz) + backup poza hostem
if [ ! -f /var/lib/solana/identity.json ]; then
sudo -u solana -H bash -lc \
"/opt/solana/bin/solana-keygen new --no-passphrase -o /var/lib/solana/identity.json"
sudo chmod 0600 /var/lib/solana/identity.json
sudo chown solana:solana /var/lib/solana/identity.json
fi
sudo -u solana -H bash -lc \
"/opt/solana/bin/solana-keygen pubkey /var/lib/solana/identity.json"
8) (Opcjonalnie, rekomendowane) Genesis hash z publicznego RPC
Mainnet:
GENESIS_HASH="$(curl -sS -H 'content-type: application/json' \
--data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getGenesisHash\"}' \
https://api.mainnet-beta.solana.com | jq -r .result)"
echo "$GENESIS_HASH"
Testnet:
GENESIS_HASH="$(curl -sS -H 'content-type: application/json' \
--data '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getGenesisHash\"}' \
https://api.testnet.solana.com | jq -r .result)"
echo "$GENESIS_HASH"
9) Systemd unit: /etc/systemd/system/agave-validator.service
Minimalny unit (RPC node, non-voting). RPC/WS bind na wg0 (10.66.66.1).
Jesli widzisz w logach:
"error: The subcommand '\\' wasn't recognized"
to zrob:
doc/step-002b-agave-validator-systemd.txt
Jesli chcesz najpierw odpalic recznie z CLI (test) przed systemd:
doc/step-002e-agave-validator-run-cli.txt
sudo tee /etc/systemd/system/agave-validator.service >/dev/null <<'EOF'
[Unit]
Description=Agave validator (mainnet, RPC-only, non-voting)
After=network-online.target wg-quick@wg0.service
Wants=network-online.target wg-quick@wg0.service
[Service]
Type=simple
User=solana
Group=solana
WorkingDirectory=/var/lib/solana
Environment=RUST_LOG=info
LimitNOFILE=1048576
Restart=always
RestartSec=5
TimeoutStopSec=120
ExecStart=/opt/solana/bin/agave-validator \
--identity /var/lib/solana/identity.json \
--no-voting \
--private-rpc \
--ledger /var/lib/solana/ledger \
--accounts /var/lib/solana/accounts \
--log /var/log/solana/validator.log \
--bind-address 0.0.0.0 \
--rpc-bind-address 10.66.66.1 \
--rpc-port 8899 \
--dynamic-port-range 8000-8020 \
--entrypoint entrypoint.mainnet-beta.solana.com:8001 \
--expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \
--full-rpc-api \
--limit-ledger-size 50000000
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/solana/ledger /var/lib/solana/accounts /var/log/solana /var/lib/solana
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now agave-validator
Jesli chcesz dodac genesis hash:
- dopisz w ExecStart linie:
--expected-genesis-hash <GENESIS_HASH>
10) Weryfikacja
systemctl is-active agave-validator
journalctl -u agave-validator -n 200 --no-pager
# z samego hosta (getHealth moze zwracac blad dopoki node nie dogoni klastra):
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getVersion"}' \
http://10.66.66.1:8899 | jq .
# z laptopa/VPS po WG:
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getSlot"}' \
http://10.66.66.1:8899 | jq .
Jesli cos nie dziala:
- `sudo wg show` (czy WG jest OK)
- `sudo ufw status verbose` (czy RPC nie jest blokowane po wg0)
- `journalctl -u agave-validator -n 200 --no-pager` (logi Agave)
Typowe fixy:
- "error: The subcommand '\\' wasn't recognized":
doc/step-002b-agave-validator-systemd.txt
- "failed to open genesis" / brak `genesis.tar.bz2`:
doc/step-002c-genesis-download-mainnet.txt
- "did you forget to provide a snapshot" (mainnet bez snapshot nie ruszy):
doc/step-002d-snapshot-download-mainnet.txt

View File

@@ -1,51 +0,0 @@
Step 002A (variant): Storage na 2x NVMe (XFS juz istnieje) -> SOLACCT=accounts, SOLLEDG=ledger
Context (wg hosta mpabi):
- /dev/nvme0n1p1: XFS LABEL=SOLACCT (nowy dysk na accounts)
- /dev/nvme1n1p1: XFS LABEL=SOLLEDG (stary dysk ~63% used na ledger)
Cel:
- NIE formatowac (bez kasowania danych), tylko zamontowac i spiac w /etc/fstab:
- /var/lib/solana/accounts -> SOLACCT
- /var/lib/solana/ledger -> SOLLEDG
0) Check (zanim dotkniesz fstab)
lsblk -f
sudo blkid /dev/nvme0n1p1 /dev/nvme1n1p1
# Upewnij sie, ze NIC nie jest zamontowane w /var/lib/solana/*:
findmnt /var/lib/solana/accounts || true
findmnt /var/lib/solana/ledger || true
1) Mountpointy
sudo getent group solana >/dev/null || sudo groupadd --system solana
sudo id -u solana >/dev/null 2>&1 || sudo useradd --system --gid solana \
--home-dir /var/lib/solana --create-home --shell /bin/bash solana
sudo install -d -o solana -g solana -m 0750 /var/lib/solana/accounts /var/lib/solana/ledger
2) /etc/fstab (po UUID)
UUID_ACCTS="$(sudo blkid -s UUID -o value /dev/nvme0n1p1)"
UUID_LEDGER="$(sudo blkid -s UUID -o value /dev/nvme1n1p1)"
echo "UUID=$UUID_ACCTS /var/lib/solana/accounts xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
echo "UUID=$UUID_LEDGER /var/lib/solana/ledger xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
sudo mount -a
sudo systemctl daemon-reload
3) Permissions (po mount) - krytyczne
sudo chown solana:solana /var/lib/solana/accounts /var/lib/solana/ledger
sudo chmod 750 /var/lib/solana/accounts /var/lib/solana/ledger
4) Weryfikacja
df -hT /var/lib/solana/accounts /var/lib/solana/ledger
findmnt /var/lib/solana/accounts
findmnt /var/lib/solana/ledger
stat -c '%U:%G %a %n' /var/lib/solana/accounts /var/lib/solana/ledger
5) Uwaga (bezpieczenstwo / dane)
- Jesli na SOLLEDG masz stare dane ledgera z innej instalacji, to validator moze startowac na "starym stanie".
Wtedy najlepiej wyczyscic ledger przed pobraniem snapshotow:
doc/step-002d-snapshot-download-mainnet.txt (sekcja czyszczenia)

View File

@@ -1,64 +0,0 @@
Step 002A: Storage pod Solana data (nvme0n1 -> XFS -> /var/lib/solana/{ledger,accounts})
Cel:
- system zostaje na md1 (/)
- dane Solany (ledger/accounts) ida na osobny NVMe: /dev/nvme0n1
UWAGA: kroki nizej KASUJA dane na /dev/nvme0n1.
0) Check (czy nvme0n1 jest do wyczyszczenia)
lsblk /dev/nvme0n1
sudo wipefs -n /dev/nvme0n1
sudo lsblk -f /dev/nvme0n1
1) Partycje (50/50)
sudo apt-get update
sudo apt-get install -y parted
sudo parted -s /dev/nvme0n1 mklabel gpt
sudo parted -s /dev/nvme0n1 mkpart primary 0% 50%
sudo parted -s /dev/nvme0n1 mkpart primary 50% 100%
sudo partprobe /dev/nvme0n1
sudo udevadm settle
lsblk /dev/nvme0n1
ls -l /dev/nvme0n1p1 /dev/nvme0n1p2
2) Format XFS
Uwaga: mkfs.xfs na Ubuntu ma limit label <= 12 znakow.
sudo mkfs.xfs -f -L sol_ledger /dev/nvme0n1p1
sudo mkfs.xfs -f -L sol_accts /dev/nvme0n1p2
sudo blkid /dev/nvme0n1p1 /dev/nvme0n1p2
3) Mountpointy + fstab (po UUID)
sudo install -d -o solana -g solana -m 0750 /var/lib/solana/ledger /var/lib/solana/accounts
UUID_LEDGER="$(sudo blkid -s UUID -o value /dev/nvme0n1p1)"
UUID_ACCTS="$(sudo blkid -s UUID -o value /dev/nvme0n1p2)"
echo "UUID=$UUID_LEDGER /var/lib/solana/ledger xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
echo "UUID=$UUID_ACCTS /var/lib/solana/accounts xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
sudo mount -a
# systemd pokazuje hint po edycji fstab - to jest OK, ale zrob reload:
sudo systemctl daemon-reload
4) WAŻNE: ustaw ownera na zamontowanych filesystemach
Po mount, root filesystemu jest domyslnie root:root 755, wiec validator uruchamiany jako solana nie zapisze ledger/accounts.
sudo chown solana:solana /var/lib/solana/ledger /var/lib/solana/accounts
sudo chmod 750 /var/lib/solana/ledger /var/lib/solana/accounts
5) Weryfikacja
df -hT /var/lib/solana/ledger /var/lib/solana/accounts
findmnt /var/lib/solana/ledger
findmnt /var/lib/solana/accounts
stat -c '%U:%G %a %n' /var/lib/solana/ledger /var/lib/solana/accounts
Dla tego hosta (z wdrozenia):
- /dev/nvme0n1p1 (LABEL=sol_ledger) UUID=56a2c342-3162-43f2-9649-e0dd25db870d
- /dev/nvme0n1p2 (LABEL=sol_accts) UUID=ae5d6625-0910-489a-bbe3-6fd156439e03

View File

@@ -1,79 +0,0 @@
Step 002B: agave-validator pod systemd (RPC-only, non-voting) + fix na problemy z ExecStart
Ten krok naprawia 2 typowe bledy po wklejeniu unit-a:
1) "error: The subcommand '\\' wasn't recognized"
2) RPC nie startuje, bo ExecStart zostal "polamany" (brakuje --rpc-port albo --log ma sciezke do katalogu)
Przyczyna:
- W unit file kontynuacja linii w ExecStart to JEDEN backslash na koncu linii: `\`
- NIE uzywaj `\\` (dwa backslashe) bo wtedy do programu trafia literalny argument `\` i validator pada.
- Dodatkowo: unikaj bardzo dlugich linii, bo clipboard/paste potrafi je rozciac (i systemd wtedy ignoruje reszte).
0) Stop/wycisz restart loop
sudo systemctl disable --now agave-validator
sudo systemctl reset-failed agave-validator
1) Wrzuc poprawny unit (wielolinijkowy ExecStart z poprawna kontynuacja `\`)
sudo tee /etc/systemd/system/agave-validator.service >/dev/null <<'EOF'
[Unit]
Description=Agave validator (mainnet, RPC-only, non-voting)
After=network-online.target wg-quick@wg0.service
Wants=network-online.target wg-quick@wg0.service
[Service]
Type=simple
User=solana
Group=solana
WorkingDirectory=/var/lib/solana
Environment=RUST_LOG=info
LimitNOFILE=1048576
Restart=always
RestartSec=5
TimeoutStopSec=120
ExecStart=/opt/solana/bin/agave-validator \
--identity /var/lib/solana/identity.json \
--no-voting \
--private-rpc \
--ledger /var/lib/solana/ledger \
--accounts /var/lib/solana/accounts \
--log /var/log/solana/validator.log \
--bind-address 0.0.0.0 \
--rpc-bind-address 10.66.66.1 \
--rpc-port 8899 \
--dynamic-port-range 8000-8025 \
--entrypoint entrypoint.mainnet-beta.solana.com:8001 \
--entrypoint entrypoint2.mainnet-beta.solana.com:8001 \
--entrypoint entrypoint3.mainnet-beta.solana.com:8001 \
--expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \
--full-rpc-api \
--limit-ledger-size 50000000 \
--geyser-plugin-config /etc/solana/yellowstone-geyser.json
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/var/lib/solana/ledger /var/lib/solana/accounts /var/log/solana /var/lib/solana
[Install]
WantedBy=multi-user.target
EOF
2) Reload + start
sudo systemctl daemon-reload
sudo systemctl enable --now agave-validator
3) Weryfikacja (na hoście)
# Sprawdz, czy systemd widzi kompletne argv[] (ma byc --rpc-port 8899 i poprawna sciezka --log):
systemctl show agave-validator -p ExecStart --no-pager
sudo systemctl status agave-validator --no-pager -l
# Czy porty sluchaja (RPC/WS na wg0):
sudo ss -lntp | egrep ':8899\\b|:8900\\b' || true
# getHealth moze zwracac blad dopoki node nie dogoni klastra, wiec sprawdz getVersion:
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getVersion"}' \
http://10.66.66.1:8899 | jq .
# Logi:
sudo journalctl -u agave-validator -n 120 --no-pager

View File

@@ -1,34 +0,0 @@
Step 002C: Download genesis (mainnet) do /var/lib/solana/ledger (fix na "failed to open genesis")
Objaw w logach:
Failed to load genesis_config ... missing genesis.bin
Extracting "/var/lib/solana/ledger/genesis.tar.bz2"...
Failed to start validator: failed to open genesis ... No such file or directory
Fix:
- Zaciagnij genesis archive na dysk i zrestartuj serwis.
1) Zatrzymaj validator (zeby nie robil restart loop)
sudo systemctl stop agave-validator
2) Pobierz genesis.tar.bz2 do ledger (jako user `solana`)
sudo -u solana -H bash -lc '
cd /var/lib/solana/ledger
rm -f genesis.tar.bz2 genesis.bin
curl -sSfL https://api.mainnet-beta.solana.com/genesis.tar.bz2 -o genesis.tar.bz2
ls -lh genesis.tar.bz2
'
Oczekiwane:
- plik genesis.tar.bz2 istnieje (ok. kilkadziesiat KB)
3) Start + logi
sudo systemctl start agave-validator
systemctl status agave-validator --no-pager -l
sudo journalctl -u agave-validator -n 80 --no-pager
4) RPC check (local/WG)
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getVersion"}' \
http://10.66.66.1:8899 | jq .

View File

@@ -1,49 +0,0 @@
Step 002D: Download snapshot (mainnet) do /var/lib/solana/ledger (wymagane na mainnet)
Objaw (bez snapshot):
Failed to start validator: ... did you forget to provide a snapshot
UWAGA:
- Snapshot full jest DUZY (setki GB). Ten krok moze trwac dlugo.
- Pobieramy full + incremental z oficjalnego endpointu (api.mainnet-beta.solana.com).
0) Stop validator
sudo systemctl stop agave-validator
1) (Opcjonalnie, polecane) wyczysc stare dane ledger (zostaw genesis.*)
sudo -u solana -H bash -lc '
cd /var/lib/solana/ledger
rm -rf rocksdb snapshots accounts_index accounts_hash_cache admin.rpc ledger.lock
ls -la | head
'
2) Pobierz FULL + INCREMENTAL snapshot do ledger (jako user `solana`)
sudo -u solana -H bash -lc '
set -euo pipefail
cd /var/lib/solana/ledger
FULL_REL="$(curl -sI https://api.mainnet-beta.solana.com/snapshot.tar.bz2 | awk "/^location:/{print \\$2}" | tr -d "\\r")"
INC_REL="$(curl -sI https://api.mainnet-beta.solana.com/incremental-snapshot.tar.bz2 | awk "/^location:/{print \\$2}" | tr -d "\\r")"
echo "FULL_REL=$FULL_REL"
echo "INC_REL=$INC_REL"
FULL_URL="https://api.mainnet-beta.solana.com${FULL_REL}"
INC_URL="https://api.mainnet-beta.solana.com${INC_REL}"
FULL_FILE="$(basename "$FULL_REL")"
INC_FILE="$(basename "$INC_REL")"
echo "Downloading FULL: $FULL_FILE"
curl -fSL "$FULL_URL" -o "$FULL_FILE"
ls -lh "$FULL_FILE"
echo "Downloading INC: $INC_FILE"
curl -fSL "$INC_URL" -o "$INC_FILE"
ls -lh "$INC_FILE"
'
3) Start validator + logi
sudo systemctl start agave-validator
sudo journalctl -u agave-validator -n 80 --no-pager

View File

@@ -1,71 +0,0 @@
Step 002E: Start agave-validator z linii polecen (test przed systemd)
Cel:
- Odpalic `agave-validator` recznie (w tmux), potwierdzic ze RPC dziala.
- Dopiero potem przepiac na systemd (Step 002B).
Preconditions:
- Masz genesis + snapshoty:
- doc/step-002c-genesis-download-mainnet.txt
- doc/step-002d-snapshot-download-mainnet.txt
- WireGuard dziala (RPC ma byc prywatne po wg0).
0) Upewnij sie, ze systemd nie trzyma procesu na portach
```bash
sudo systemctl stop agave-validator || true
sudo systemctl disable agave-validator || true
```
1) Odpal w tmux (zeby proces nie zginal po rozlaczeniu SSH)
```bash
tmux new -s agave-rpc
```
2) Start (dzialajaca komenda z wdrozenia)
```bash
sudo bash -lc '
set -euo pipefail
ulimit -n 1048576
exec sudo -u solana -H /opt/solana/bin/agave-validator \
--identity /var/lib/solana/identity.json \
--no-voting \
--private-rpc \
--ledger /var/lib/solana/ledger \
--accounts /var/lib/solana/accounts \
--log - \
--bind-address 0.0.0.0 \
--rpc-bind-address 10.66.66.1 \
--rpc-port 8899 \
--dynamic-port-range 8000-8020 \
--entrypoint entrypoint.mainnet-beta.solana.com:8001 \
--entrypoint entrypoint2.mainnet-beta.solana.com:8001 \
--entrypoint entrypoint3.mainnet-beta.solana.com:8001 \
--expected-genesis-hash 5eykt4UsFv8P8NJdTREpY1vzqKqZKvdpKuc147dw2N9d \
--full-rpc-api \
--limit-ledger-size 50000000
'
```
3) Weryfikacja (w drugim oknie / innym terminalu)
```bash
ss -lnt | egrep ':8899\\b|:8900\\b' || true
```
```bash
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getVersion"}' \
http://10.66.66.1:8899 | jq .
```
4) Detach z tmux
- `Ctrl+b d`
5) Stop (gdy chcesz zatrzymac recznie)
- `tmux attach -t agave-rpc`
- `Ctrl+c` (zatrzyma validator)
- `exit`
Next:
- Jak komenda dziala stabilnie, wrzuc unit i odpal przez systemd:
doc/step-002b-agave-validator-systemd.txt

View File

@@ -1,70 +0,0 @@
Step 002F: Build Agave (agave-validator) z najnowszych zrodel (pin do releasu) i instalacja do /opt/solana/bin
Cel:
- Zbudowac `agave-validator` ze zrodel z pinu do konkretnego releasu (stable dla mainnet).
- Nie robic "floating HEAD" (zeby nie wjechal update bez kontroli).
Konwencja:
- "najnowsza wersja" = najnowszy stabilny release dla mainnet z GitHub Releases repo `anza-xyz/agave`.
(Zweryfikuj przed startem: tag typu v3.0.x.)
0) Packages (build deps)
sudo apt-get update
sudo apt-get install -y \
build-essential pkg-config libssl-dev \
clang llvm-dev libclang-dev \
cmake protobuf-compiler libudev-dev zlib1g-dev \
curl jq git ca-certificates
1) Uzytkownik + katalogi
sudo getent group solana >/dev/null || sudo groupadd --system solana
sudo id -u solana >/dev/null 2>&1 || sudo useradd --system --gid solana \
--home-dir /var/lib/solana --create-home --shell /bin/bash solana
sudo install -d -o root -g root -m 0755 /opt/solana/bin
sudo install -d -o solana -g solana -m 0750 /var/lib/solana/src
2) Wybierz tag releasu (stable dla mainnet)
# Opcja A (manual): sprawdz Releases i wpisz tag, np.:
AGAVE_VERSION_TAG="v3.0.13"
# Opcja B (CLI): pobierz liste tagow i wybierz najwyzszy semver (bez rc/beta)
# (Uwaga: to korzysta z network; jesli nie masz neta - uzyj opcji A.)
AGAVE_VERSION_TAG="$(
git ls-remote --refs --tags https://github.com/anza-xyz/agave.git 'v*' \
| awk -F/ '{print $NF}' \
| rg -v '(rc|beta|alpha)' \
| sort -V \
| tail -n1
)"
echo "AGAVE_VERSION_TAG=$AGAVE_VERSION_TAG"
3) Rust toolchain
# Rekomendacja: uzyj rustup (Agave ma pinned toolchain w repo).
sudo -u solana -H bash -lc 'command -v rustup >/dev/null 2>&1 || (curl -sSfL https://sh.rustup.rs | sh -s -- -y)'
4) Clone + build (pin do tagu)
AGAVE_SRC_DIR="/var/lib/solana/src/agave-${AGAVE_VERSION_TAG}"
sudo -u solana -H bash -lc "
set -euo pipefail
export PATH=/var/lib/solana/.cargo/bin:\$PATH
rm -rf \"${AGAVE_SRC_DIR}\"
git clone --depth 1 --branch \"${AGAVE_VERSION_TAG}\" https://github.com/anza-xyz/agave.git \"${AGAVE_SRC_DIR}\"
cd \"${AGAVE_SRC_DIR}\"
# Agave ma wrapper `./cargo`, ktory bierze pinned toolchain z repo.
./cargo build --release -p agave-validator
"
5) Install do /opt/solana/bin
sudo install -o root -g root -m 0755 \
"${AGAVE_SRC_DIR}/target/release/agave-validator" \
/opt/solana/bin/agave-validator
/opt/solana/bin/agave-validator --version
6) Uwagi (operacyjne)
- Jesli budujesz pluginy (Geyser), trzymaj je na tym samym pinned releasie i najlepiej w tej samej "epoce" API.
- Po update do nowego releasu: buduj validator + pluginy ponownie i restartuj systemd.

View File

@@ -1,264 +0,0 @@
Runbook (mpabi): Agave RPC (mainnet, private) + Geyser plugin -> Postgres
Cel:
- Na mpabi odpalic prywatny Agave RPC (non-voting) po WireGuard (wg0).
- Zbudowac Agave ze zrodel (pinned release).
- (Po starcie) dodac Geyser plugin zapisujacy do PostgreSQL.
Skrot (kolejnosc):
1) Preconditions: WG + UFW (SSH tylko po wg0).
2) Storage: SOLACCT->/var/lib/solana/accounts, SOLLEDG->/var/lib/solana/ledger (+ decyzja: wipe czy kontynuacja).
3) Pakiety + sysctl/limits.
4) /opt/solana/bin: Anza tools (solana-keygen itd.).
5) Build Agave ze zrodel (pin tag) + install do /opt/solana/bin/agave-validator.
6) Identity (0600).
7) Genesis + snapshot (mainnet).
8) Systemd: agave-validator (User=solana) + RPC smoke test po WG.
9) Geyser plugin -> Postgres + dopiecie do systemd + weryfikacja.
Konwencja:
- Komendy uruchamiasz jako user: `user` (z sudo).
- Validator dziala jako uzytkownik systemowy: `solana`.
- Biny instalujemy do /opt/solana/bin (root-owned, stabilne sciezki).
- Dane runtime:
- ledger: /var/lib/solana/ledger (SOLLEDG = /dev/nvme1n1p1)
- accounts: /var/lib/solana/accounts (SOLACCT = /dev/nvme0n1p1)
Parametry hosta (mpabi):
- Public iface: enp6s0, public IP: 149.50.116.219
- WireGuard iface: wg0, WG IP: 10.66.66.1
- RPC: 8899, WS: 8900
- Cluster inbound UDP: 8000-8020 (na public)
=====================
0) Preconditions
=====================
1) WireGuard up:
ip -4 a show wg0
wg show
2) SSH powinno byc tylko po wg0 (defense-in-depth):
sudo ufw status verbose
3) Upewnij sie, ze masz sudo do operacji operatorskich (systemctl/journalctl/ufw).
=====================
1) Storage: mount SOLACCT/SOLLEDG
=====================
UWAGA: ten wariant NIE formatuje dyskow (bez kasowania), tylko montuje istniejace XFS.
UUIDs (wg lsblk -f):
- SOLACCT: 055bf4e7-00f0-45a0-a995-2413b14428c4 (/dev/nvme0n1p1)
- SOLLEDG: 6ac7a8c6-efc7-4803-a3dc-a66ea2ef0125 (/dev/nvme1n1p1)
1) (Opcjonalnie) obejrzyj co zajmuje SOLLEDG (zanim podepniesz pod /var/lib/solana/ledger):
sudo mkdir -p /mnt/solledg
sudo mount /dev/nvme1n1p1 /mnt/solledg
sudo du -sh /mnt/solledg/* 2>/dev/null | sort -h | tail
sudo umount /mnt/solledg
2) Utworz usera solana + mountpointy:
sudo getent group solana >/dev/null || sudo groupadd --system solana
sudo id -u solana >/dev/null 2>&1 || sudo useradd --system --gid solana \
--home-dir /var/lib/solana --create-home --shell /bin/bash solana
sudo install -d -o solana -g solana -m 0750 /var/lib/solana/accounts /var/lib/solana/ledger
3) Dodaj wpisy do /etc/fstab (sprawdz czy nie ma juz duplikatow):
grep -nE '(/var/lib/solana/(accounts|ledger))' /etc/fstab || true
UUID_ACCTS="055bf4e7-00f0-45a0-a995-2413b14428c4"
UUID_LEDGER="6ac7a8c6-efc7-4803-a3dc-a66ea2ef0125"
echo "UUID=$UUID_ACCTS /var/lib/solana/accounts xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
echo "UUID=$UUID_LEDGER /var/lib/solana/ledger xfs noatime,nodiratime 0 2" | sudo tee -a /etc/fstab >/dev/null
sudo mount -a
sudo systemctl daemon-reload
4) Permissions (krytyczne):
sudo chown solana:solana /var/lib/solana/accounts /var/lib/solana/ledger
sudo chmod 750 /var/lib/solana/accounts /var/lib/solana/ledger
5) Verify:
df -hT /var/lib/solana/accounts /var/lib/solana/ledger
findmnt /var/lib/solana/accounts
findmnt /var/lib/solana/ledger
stat -c '%U:%G %a %n' /var/lib/solana/accounts /var/lib/solana/ledger
DECYZJA: co robimy z SOLLEDG (63% used)?
- Opcja A: zostawiamy (jesli to stary ledger Solany i chcesz kontynuowac).
- Opcja B: czyscimy i robimy sync od nowa (destrukcyjne, rekomendowane dla powtarzalnosci):
sudo systemctl stop agave-validator 2>/dev/null || true
sudo -u solana -H bash -lc '
cd /var/lib/solana/ledger
rm -rf rocksdb snapshots accounts_index accounts_hash_cache admin.rpc ledger.lock
'
=====================
2) Pakiety + sysctl/limits
=====================
1) Pakiety:
sudo apt-get update
sudo apt-get install -y \
chrony curl jq git ca-certificates ripgrep \
smartmontools nvme-cli \
build-essential pkg-config libssl-dev \
clang llvm-dev libclang-dev \
cmake protobuf-compiler libudev-dev zlib1g-dev
2) Sysctl:
sudo tee /etc/sysctl.d/90-agave.conf >/dev/null <<'EOF'
fs.file-max = 2000000
vm.max_map_count = 1000000
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 250000
net.core.rmem_max = 134217728
net.core.wmem_max = 134217728
EOF
sudo sysctl --system
3) ulimit:
sudo tee /etc/security/limits.d/90-solana.conf >/dev/null <<'EOF'
* soft nofile 1000000
* hard nofile 1000000
EOF
=====================
3) Firewall (UFW) dla prywatnego RPC
=====================
Docelowo:
- SSH 22/tcp: tylko wg0
- RPC 8899/tcp, WS 8900/tcp: tylko wg0 (dodatkowo bind na 10.66.66.1)
- WireGuard: 51820/udp (public)
- Cluster sync: 8000-8020/udp (public)
Komendy (jesli jeszcze nie ustawione):
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 51820/udp
sudo ufw allow in on wg0 to any port 22 proto tcp
sudo ufw deny in on enp6s0 to any port 22 proto tcp
sudo ufw allow in on wg0 to any port 8899 proto tcp
sudo ufw allow in on wg0 to any port 8900 proto tcp
sudo ufw deny in on enp6s0 to any port 8899 proto tcp
sudo ufw deny in on enp6s0 to any port 8900 proto tcp
sudo ufw allow in on enp6s0 to any port 8000:8020 proto udp
sudo ufw enable
sudo ufw status verbose
=====================
4) Instalacja narzedzi Solana/Anza do /opt/solana/bin
=====================
1) Installer (jako solana):
sudo install -d -o root -g root -m 0755 /opt/solana/bin
sudo -u solana -H bash -lc 'sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)"'
2) Skopiuj biny do /opt/solana/bin:
sudo cp -a /var/lib/solana/.local/share/solana/install/active_release/bin/. /opt/solana/bin/
sudo find /opt/solana/bin -maxdepth 1 -type f -exec chmod 0755 {} \;
3) Verify:
/opt/solana/bin/solana --version || true
/opt/solana/bin/solana-keygen --version
=====================
5) Build + install agave-validator (pinned latest stable)
=====================
Patrz:
- trade-iac/doc/step-002f-agave-build-from-source-latest.txt
Skrót:
1) Wybierz TAG (np. v3.0.x) i zbuduj jako solana do /var/lib/solana/src/
2) Zainstaluj binarke do /opt/solana/bin/agave-validator
3) Verify:
/opt/solana/bin/agave-validator --version
=====================
6) Identity
=====================
1) Utworz raz:
if [ ! -f /var/lib/solana/identity.json ]; then
sudo -u solana -H /opt/solana/bin/solana-keygen new --no-passphrase -o /var/lib/solana/identity.json
sudo chown solana:solana /var/lib/solana/identity.json
sudo chmod 0600 /var/lib/solana/identity.json
fi
2) Zapisz pubkey do notatek:
sudo -u solana -H /opt/solana/bin/solana-keygen pubkey /var/lib/solana/identity.json
=====================
7) Genesis + snapshot (mainnet)
=====================
1) Genesis:
patrz: trade-iac/doc/step-002c-genesis-download-mainnet.txt
2) Snapshot (wymagany na mainnet):
patrz: trade-iac/doc/step-002d-snapshot-download-mainnet.txt
=====================
8) Systemd: agave-validator (RPC-only)
=====================
Unit template:
- trade-iac/doc/step-002b-agave-validator-systemd.txt
Wazne:
- `User=solana`
- `--rpc-bind-address 10.66.66.1`
- `--private-rpc`
- nie uzywaj `\\` w ExecStart (tylko pojedynczy `\` na koncu linii)
Start/verify:
sudo systemctl daemon-reload
sudo systemctl enable --now agave-validator
systemctl is-active agave-validator
sudo journalctl -u agave-validator -n 120 --no-pager
RPC smoke test (po WG):
curl -sS -H 'content-type: application/json' \
--data '{"jsonrpc":"2.0","id":1,"method":"getVersion"}' \
http://10.66.66.1:8899 | jq .
Manual run (exports + komenda w plikach na mpabi):
# Zawiera "solana export" (PATH + SOLANA_RPC_URL) i helper `agave_run()`:
# - /etc/solana/agave-env.sh
# - /etc/solana/agave-validator.args
sudo -u solana -H bash -lc 'source /etc/solana/agave-env.sh; agave_run'
=====================
9) Geyser plugin -> PostgreSQL
=====================
Krok po kroku:
- trade-iac/doc/step-003-geyser-plugin-postgres.txt
Skrót:
1) Postgres:
sudo apt-get install -y postgresql
sudo systemctl enable --now postgresql
2) DB/user:
sudo -u postgres psql -v ON_ERROR_STOP=1 <<'SQL'
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'geyser') THEN
CREATE ROLE geyser LOGIN PASSWORD 'CHANGE_ME';
END IF;
END $$;
CREATE DATABASE geyser OWNER geyser;
SQL
3) Build plugin (.so) i instaluj do /opt/solana/plugins/
4) Skopiuj example JSON config do /etc/solana/geyser-postgres.json, ustaw DSN, selektory.
5) Dopisz do unit ExecStart:
--geyser-plugin-config /etc/solana/geyser-postgres.json
i zrestartuj:
sudo systemctl daemon-reload
sudo systemctl restart agave-validator
Verify:
sudo journalctl -u agave-validator -n 200 --no-pager | rg -i 'geyser|plugin|postgres' || true
sudo -u postgres psql -d geyser -c '\\dt' | head

View File

@@ -1,122 +0,0 @@
Step 003: Geyser plugin -> PostgreSQL (Agave) na prywatnym RPC node
Cel:
- Wlaczyc Geyser plugin zapisujacy eventy do Postgresa.
- Na poczatek: dzialajace end-to-end (plugin laduje sie w validatorze + dane wpadaja do DB).
Wazne ryzyka:
- Pisanie wszystkiego do Postgresa na mainnet generuje OGROMNY write load.
Jezeli DB bedzie na tym samym hoście co validator, latwo o IO contention i spadek performance sync.
- Rekomendacja: startuj od selektorow (filtrow), np. tylko programy ktore bot potrzebuje.
0) Preconditions
- Agave dziala pod systemd:
- doc/step-002b-agave-validator-systemd.txt
- Masz pinned Agave release (tag) i go nie zmieniasz "w locie":
- doc/step-002f-agave-build-from-source-latest.txt
1) Postgres (lokalnie, minimalny setup)
sudo apt-get update
sudo apt-get install -y postgresql postgresql-contrib
sudo systemctl enable --now postgresql
# DB + user (dostosuj haslo; nie commituj do repo)
sudo -u postgres psql -v ON_ERROR_STOP=1 <<'SQL'
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'geyser') THEN
CREATE ROLE geyser LOGIN PASSWORD 'CHANGE_ME';
END IF;
END $$;
CREATE DATABASE geyser OWNER geyser;
SQL
2) Build plugin ze zrodel (Anza Agave plugin Postgres)
# Repo:
# https://github.com/anza-xyz/agave-geyser-plugin-postgres
#
# Uwaga: plugin i validator musza byc kompatybilne wersjami interfejsu.
# Najbezpieczniej: build pluginu po update Agave i test "plugin loaded".
#
# Opcjonalnie: pin plugin do tagu/commit (np. pod ten sam release co Agave).
# PLUGIN_REF="main" # albo tag/commit
sudo install -d -o root -g root -m 0755 /opt/solana/plugins
sudo install -d -o solana -g solana -m 0750 /var/lib/solana/src
PLUGIN_SRC_DIR="/var/lib/solana/src/agave-geyser-plugin-postgres"
sudo -u solana -H bash -lc "
set -euo pipefail
export PATH=/var/lib/solana/.cargo/bin:\$PATH
rm -rf \"${PLUGIN_SRC_DIR}\"
git clone https://github.com/anza-xyz/agave-geyser-plugin-postgres.git \"${PLUGIN_SRC_DIR}\"
cd \"${PLUGIN_SRC_DIR}\"
if [ \"${PLUGIN_REF:-main}\" != \"main\" ]; then git checkout \"${PLUGIN_REF}\"; fi
cargo build --release
"
# Typowa nazwa .so (sprawdz `ls target/release/*.so`):
sudo cp -a "${PLUGIN_SRC_DIR}"/target/release/*.so /opt/solana/plugins/
sudo chmod 0755 /opt/solana/plugins/*.so
3) Schema w Postgres
# Plugin repo ma SQL do stworzenia schematu. Najpierw znajdz plik:
sudo -u solana -H bash -lc '
cd /var/lib/solana/src/agave-geyser-plugin-postgres
find . -maxdepth 3 -type f -iname \"*schema*.sql\" -o -type f -iname \"create*.sql\" | sort
'
# Potem wykonaj jako postgres na DB geyser (podmien sciezke na znaleziony plik):
# sudo -u postgres psql -d geyser -f /var/lib/solana/src/agave-geyser-plugin-postgres/scripts/create_schema.sql
4) Konfig pluginu (JSON)
# Najbezpieczniej: skopiuj example config z repo pluginu i zmien tylko:
# - sciezke do .so
# - connection string
# - selektory (na poczatek mozesz dac \"*\" tylko na smoke test, potem zawęz)
#
# Znajdz przyklad w repo:
sudo -u solana -H bash -lc '
cd /var/lib/solana/src/agave-geyser-plugin-postgres
find . -maxdepth 4 -type f -iname \"*.json\" | sort
rg -n \"connection_str|libpath|accounts_selector|transaction_selector|selector\" -S . || true
'
sudo install -d -o root -g root -m 0755 /etc/solana
# Start od skopiowania example (podmien sciezke jesli repo ma inna nazwe pliku):
# sudo cp -a /var/lib/solana/src/agave-geyser-plugin-postgres/config.json /etc/solana/geyser-postgres.json
# Uprawnienia: config zawiera haslo, ale validator (User=solana) musi go czytac.
if [ -f /etc/solana/geyser-postgres.json ]; then
sudo chown root:solana /etc/solana/geyser-postgres.json
sudo chmod 0640 /etc/solana/geyser-postgres.json
fi
5) Wlaczenie pluginu w agave-validator (systemd)
# Sprawdz, jak na Twojej wersji nazywa sie flaga:
/opt/solana/bin/agave-validator --help | rg -n \"geyser|plugin\" || true
# Docelowo (w unit file) dopisz:
# --geyser-plugin-config /etc/solana/geyser-postgres.json
#
# Nastepnie:
sudo systemctl daemon-reload
sudo systemctl restart agave-validator
6) Weryfikacja
sudo journalctl -u agave-validator -n 200 --no-pager | rg -i \"geyser|plugin|postgres\" || true
# Czy w DB pojawiaja sie tabele (zalezy od schema.sql):
sudo -u postgres psql -d geyser -c \"\\dt\" | head
# Czy rosną inserty (przyklad - dostosuj nazwy tabel):
# sudo -u postgres psql -d geyser -c 'select count(*) from account;'
7) Performance (minimum sanity)
- Jezeli validator zacznie znacząco odstawać, zacznij od:
- zawężenia selektorow (unikaj \"*\" po smoke tescie)
- zmniejszenia batch/threads
- przeniesienia DB na osobny host (lub przynajmniej na osobny NVMe)

67
doc/workflow.md Normal file
View File

@@ -0,0 +1,67 @@
# Workflow `trade-iac`
Cel: utrzymywać konfigurację IaC w Git i wdrażać ją kontrolowanie na `mevnode`.
## 1. Dodanie klucza SSH na hoście (operator)
Host operatora musi mieć klucz do dostępu do repo `trade/trade-iac`.
Przykład:
```bash
ssh-keygen -t ed25519 -C "trade-iac-host" -f ~/.ssh/trade_iac
cat ~/.ssh/trade_iac.pub
```
Publiczny klucz dodajemy w Gitei (`Settings -> SSH Keys`), a potem test:
```bash
ssh -T git@gitea.mpabi.pl
```
## 2. Dodanie klucza SSH na VPS
VPS (runner/deployer) musi mieć osobny klucz do klonowania/pull z `trade/trade-iac`.
Przykład:
```bash
ssh-keygen -t ed25519 -C "trade-iac-vps" -f ~/.ssh/trade_iac
cat ~/.ssh/trade_iac.pub
ssh -T git@gitea.mpabi.pl
```
Ten publiczny klucz też dodajemy do Gitei.
## 3. Wdrożenie na `mevnode` (runner na VPS)
Po zmianach w repo:
1. `doc/` (opis etapu),
2. implementacja (playbook/vars),
3. test/syntax-check,
4. commit + push,
5. wdrożenie z VPS na `mevnode`,
6. testy powdrożeniowe.
Minimalny przepływ:
```bash
# VPS
git -C /opt/trade-iac pull --ff-only origin main
cd /opt/trade-iac
# uruchom Ansible w kontenerze (bez instalacji Ansible na hoście)
docker run --rm -t \
-v "$PWD/ansible:/ansible" \
-v "$HOME/.ssh:/home/runner/.ssh:ro" \
-w /ansible \
quay.io/ansible/ansible-runner:latest \
ansible-playbook -i inventory/hosts.ini playbooks/doc-rpc-sol-min.yml
```
## Zasada pracy
- Zmiany zawsze przez PR/commit (bez ręcznych zmian na `mevnode` poza awarią).
- `mevnode` traktujemy jako target deploymentu, nie źródło prawdy.
- Źródłem prawdy jest repo `trade/trade-iac`.

View File

@@ -1,93 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path
from datetime import datetime
WG_DIR = Path("/etc/wireguard")
WG0 = WG_DIR / "wg0.conf"
def require_root():
if os.geteuid() != 0:
print("ERROR: run as root (sudo).", file=sys.stderr)
sys.exit(1)
def run(cmd, check=True, capture=False):
return subprocess.run(
cmd,
check=check,
text=True,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
)
def backup_if_exists(p: Path):
if p.exists():
ts = datetime.now().strftime("%Y-%m-%d_%H%M%S")
b = p.with_name(p.name + f".bak.{ts}")
shutil.copy2(p, b)
def cmd_install(src: str):
srcp = Path(src)
if not srcp.exists():
raise RuntimeError(f"Missing source config: {srcp}")
WG_DIR.mkdir(parents=True, exist_ok=True)
os.chmod(WG_DIR, 0o700)
backup_if_exists(WG0)
shutil.copy2(srcp, WG0)
os.chmod(WG0, 0o600)
os.chown(WG0, 0, 0)
print(f"OK: installed {WG0}")
def cmd_up():
run(["wg-quick", "up", "wg0"], check=True)
def cmd_down():
run(["wg-quick", "down", "wg0"], check=False)
def cmd_status():
run(["wg", "show"], check=False)
run(["ip", "a", "show", "wg0"], check=False)
def main():
require_root()
p = argparse.ArgumentParser(prog="wg_client.py")
sub = p.add_subparsers(dest="cmd", required=True)
i = sub.add_parser("install")
i.add_argument("config", help="Path to clientX.conf from server export")
sub.add_parser("up")
sub.add_parser("down")
sub.add_parser("status")
args = p.parse_args()
try:
if args.cmd == "install":
cmd_install(args.config)
elif args.cmd == "up":
cmd_up()
elif args.cmd == "down":
cmd_down()
elif args.cmd == "status":
cmd_status()
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,419 +0,0 @@
#!/usr/bin/env python3
import argparse
import os
import re
import shutil
import subprocess
import sys
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Optional, List
WG_KEY_RE = re.compile(r"^[A-Za-z0-9+/]{43}=$")
@dataclass
class Cfg:
wg_dir: Path = Path("/etc/wireguard")
clients_dir: Path = Path("/etc/wireguard/clients")
server_key: Path = Path("/etc/wireguard/server.key")
server_pub: Path = Path("/etc/wireguard/server.pub")
wg0_conf: Path = Path("/etc/wireguard/wg0.conf")
wg_addr: str = "10.66.66.1/24"
wg_port: int = 51820
keepalive: int = 25
client_net_prefix: str = "10.66.66."
client_ip_start: int = 2
client_ip_end: int = 254
def require_root():
if os.geteuid() != 0:
print("ERROR: run as root (sudo).", file=sys.stderr)
sys.exit(1)
def run(
cmd: List[str],
capture: bool = False,
check: bool = True,
input_text: Optional[str] = None,
) -> subprocess.CompletedProcess:
return subprocess.run(
cmd,
text=True,
input=input_text,
stdout=subprocess.PIPE if capture else None,
stderr=subprocess.PIPE if capture else None,
check=check,
)
def ensure_dirs(cfg: Cfg):
cfg.wg_dir.mkdir(parents=True, exist_ok=True)
cfg.clients_dir.mkdir(parents=True, exist_ok=True)
os.chmod(cfg.wg_dir, 0o700)
os.chmod(cfg.clients_dir, 0o700)
def read_first_nonempty_line(p: Path) -> str:
if not p.exists():
return ""
for line in p.read_text(encoding="utf-8", errors="ignore").splitlines():
line = line.strip().replace("\r", "")
if line:
return line
return ""
def validate_key(k: str, what: str):
if not WG_KEY_RE.match(k):
raise ValueError(f"{what} has invalid WireGuard key format: {k!r}")
def chmod600_root(p: Path):
os.chmod(p, 0o600)
os.chown(p, 0, 0)
def backup_if_exists(p: Path):
if p.exists():
ts = datetime.now().strftime("%Y-%m-%d_%H%M%S")
b = p.with_name(p.name + f".bak.{ts}")
shutil.copy2(p, b)
def gen_priv_key_to(path: Path):
priv = run(["wg", "genkey"], capture=True).stdout.strip()
validate_key(priv, "generated private key")
path.write_text(priv + "\n", encoding="utf-8")
chmod600_root(path)
def pubkey_from_priv(priv: str) -> str:
validate_key(priv, "private key")
pub = run(["wg", "pubkey"], capture=True, input_text=priv + "\n").stdout.strip()
validate_key(pub, "generated public key")
return pub
def detect_public_ip() -> str:
# Works on Ubuntu/Debian in typical setups
out = run(
[
"bash",
"-lc",
"ip route get 1.1.1.1 | awk '{for (i=1;i<=NF;i++) if ($i==\"src\") {print $(i+1); exit}}'",
],
capture=True,
).stdout.strip()
if not out:
raise RuntimeError("Could not detect server public IP.")
return out
def restart_wg(unit: str = "wg-quick@wg0"):
run(["systemctl", "restart", unit], capture=False, check=True)
# optional status (non-fatal)
try:
run(["systemctl", "--no-pager", "-l", "status", unit], capture=False, check=False)
except Exception:
pass
def list_clients(cfg: Cfg) -> List[str]:
pubs = sorted(cfg.clients_dir.glob("*.pub"))
return [p.stem for p in pubs]
def used_octets(cfg: Cfg) -> set:
s = set()
for f in cfg.clients_dir.glob("*.ip"):
v = read_first_nonempty_line(f)
if v.isdigit():
s.add(int(v))
return s
def alloc_octet(cfg: Cfg) -> int:
used = used_octets(cfg)
for i in range(cfg.client_ip_start, cfg.client_ip_end + 1):
if i not in used:
return i
raise RuntimeError("No free IPs left in pool.")
def gen_server(cfg: Cfg, force: bool):
ensure_dirs(cfg)
if not force and (cfg.server_key.exists() or cfg.server_pub.exists()):
raise RuntimeError("Server key already exists. Use --force to overwrite.")
gen_priv_key_to(cfg.server_key)
priv = read_first_nonempty_line(cfg.server_key)
pub = pubkey_from_priv(priv)
cfg.server_pub.write_text(pub + "\n", encoding="utf-8")
chmod600_root(cfg.server_pub)
print("OK: generated server keys:")
print(f" - {cfg.server_key} (PRIVATE)")
print(f" - {cfg.server_pub} (PUBLIC)")
print("Server public key:")
print(pub)
def gen_client(cfg: Cfg, name: str, force: bool):
ensure_dirs(cfg)
keyf = cfg.clients_dir / f"{name}.key"
pubf = cfg.clients_dir / f"{name}.pub"
if not force and (keyf.exists() or pubf.exists()):
raise RuntimeError(f"Client {name} exists. Use --force to overwrite.")
gen_priv_key_to(keyf)
priv = read_first_nonempty_line(keyf)
pub = pubkey_from_priv(priv)
pubf.write_text(pub + "\n", encoding="utf-8")
chmod600_root(pubf)
print("OK: generated client keys:")
print(f" - {keyf} (PRIVATE)")
print(f" - {pubf} (PUBLIC)")
print("Client public key:")
print(pub)
def generate_wg0_conf(cfg: Cfg):
ensure_dirs(cfg)
if not cfg.server_key.exists():
raise RuntimeError("Missing server.key. Run gen-server first.")
server_priv = read_first_nonempty_line(cfg.server_key)
validate_key(server_priv, "server private key")
backup_if_exists(cfg.wg0_conf)
lines: List[str] = []
lines += [
"[Interface]",
f"Address = {cfg.wg_addr}",
f"ListenPort = {cfg.wg_port}",
f"PrivateKey = {server_priv}",
]
for name in sorted(list_clients(cfg)):
pubf = cfg.clients_dir / f"{name}.pub"
ipf = cfg.clients_dir / f"{name}.ip"
if not ipf.exists():
raise RuntimeError(f"Client {name} has no .ip file (run add {name}).")
pub = read_first_nonempty_line(pubf)
validate_key(pub, f"client {name} pubkey")
octet = read_first_nonempty_line(ipf)
if not octet.isdigit():
raise RuntimeError(f"Invalid ip octet in {ipf}: {octet!r}")
o = int(octet)
if o < 2 or o > 254:
raise RuntimeError(f"Invalid ip octet in {ipf}: {o}")
lines += [
"",
"[Peer]",
f"# {name}",
f"PublicKey = {pub}",
f"AllowedIPs = {cfg.client_net_prefix}{o}/32",
f"PersistentKeepalive = {cfg.keepalive}",
]
cfg.wg0_conf.write_text("\n".join(lines) + "\n", encoding="utf-8")
chmod600_root(cfg.wg0_conf)
print(f"OK: generated {cfg.wg0_conf}")
def add_client(cfg: Cfg, name: str, pubkey: Optional[str], ip: Optional[str], no_restart: bool):
ensure_dirs(cfg)
pubf = cfg.clients_dir / f"{name}.pub"
ipf = cfg.clients_dir / f"{name}.ip"
# resolve pubkey
if pubkey:
pubkey = pubkey.strip()
validate_key(pubkey, "provided client public key")
pubf.write_text(pubkey + "\n", encoding="utf-8")
chmod600_root(pubf)
else:
if not pubf.exists():
raise RuntimeError(f"Missing {pubf}. Run gen-client {name} or provide --pubkey.")
pubkey = read_first_nonempty_line(pubf)
validate_key(pubkey, f"client {name} pubkey")
# resolve ip octet
if ip is None:
octet = alloc_octet(cfg)
else:
ip = ip.strip()
if ip.startswith(cfg.client_net_prefix):
ip = ip[len(cfg.client_net_prefix) :]
if not ip.isdigit():
raise RuntimeError("--ip must be an octet (2..254) or full IP like 10.66.66.X")
octet = int(ip)
if octet < 2 or octet > 254:
raise RuntimeError("--ip must be 2..254")
if octet in used_octets(cfg):
raise RuntimeError(f"IP octet {octet} already used")
ipf.write_text(f"{octet}\n", encoding="utf-8")
chmod600_root(ipf)
print(f"OK: added peer {name} -> {cfg.client_net_prefix}{octet}/32")
generate_wg0_conf(cfg)
if not no_restart:
restart_wg()
def del_client(cfg: Cfg, name: str, no_restart: bool):
ensure_dirs(cfg)
pubf = cfg.clients_dir / f"{name}.pub"
ipf = cfg.clients_dir / f"{name}.ip"
# keep .key on purpose
if pubf.exists():
pubf.unlink()
if ipf.exists():
ipf.unlink()
print(f"OK: removed peer {name} (pub/ip). Private key kept if existed.")
generate_wg0_conf(cfg)
if not no_restart:
restart_wg()
def export_client_conf(cfg: Cfg, name: str, endpoint: Optional[str]):
ensure_dirs(cfg)
keyf = cfg.clients_dir / f"{name}.key"
ipf = cfg.clients_dir / f"{name}.ip"
if not keyf.exists():
raise RuntimeError(f"Missing {keyf}. Run gen-client {name} on server or create key on client.")
if not ipf.exists():
raise RuntimeError(f"Missing {ipf}. Run add {name} first.")
if not cfg.server_pub.exists():
raise RuntimeError("Missing server.pub. Run gen-server first.")
client_priv = read_first_nonempty_line(keyf)
validate_key(client_priv, f"client {name} private key")
server_pub = read_first_nonempty_line(cfg.server_pub)
validate_key(server_pub, "server public key")
octet = int(read_first_nonempty_line(ipf))
if endpoint is None:
endpoint = f"{detect_public_ip()}:{cfg.wg_port}"
out = cfg.clients_dir / f"{name}.conf"
content = "\n".join(
[
"[Interface]",
f"Address = {cfg.client_net_prefix}{octet}/32",
f"PrivateKey = {client_priv}",
"",
"[Peer]",
f"PublicKey = {server_pub}",
f"Endpoint = {endpoint}",
# Default: only reach the server's WG IP via tunnel.
"AllowedIPs = 10.66.66.1/32",
f"PersistentKeepalive = {cfg.keepalive}",
"",
]
)
out.write_text(content, encoding="utf-8")
chmod600_root(out)
print(f"OK: wrote {out}")
def cmd_list(cfg: Cfg):
ensure_dirs(cfg)
names = list_clients(cfg)
if not names:
print("(no clients)")
return
for n in names:
ipf = cfg.clients_dir / f"{n}.ip"
octet = read_first_nonempty_line(ipf) if ipf.exists() else "?"
print(f"{n}\t{cfg.client_net_prefix}{octet}/32")
def cmd_show(cfg: Cfg, name: str):
ensure_dirs(cfg)
pubf = cfg.clients_dir / f"{name}.pub"
ipf = cfg.clients_dir / f"{name}.ip"
keyf = cfg.clients_dir / f"{name}.key"
print(f"Name: {name}")
if ipf.exists():
print(f"IP: {cfg.client_net_prefix}{read_first_nonempty_line(ipf)}/32")
else:
print("IP: (not assigned)")
if pubf.exists():
print(f"PUB: {read_first_nonempty_line(pubf)}")
else:
print("PUB: (missing)")
print(f"KEY: {'exists' if keyf.exists() else 'missing'} ({keyf})")
def main():
require_root()
cfg = Cfg()
p = argparse.ArgumentParser(prog="wg_serverctl.py")
sub = p.add_subparsers(dest="cmd", required=True)
s = sub.add_parser("gen-server")
s.add_argument("--force", action="store_true")
g = sub.add_parser("gen-client")
g.add_argument("name")
g.add_argument("--force", action="store_true")
a = sub.add_parser("add")
a.add_argument("name")
a.add_argument("--pubkey", help="Client public key (base64, ends with =)")
a.add_argument("--ip", help="IP octet (2..254) or full 10.66.66.X")
a.add_argument("--no-restart", action="store_true")
d = sub.add_parser("del")
d.add_argument("name")
d.add_argument("--no-restart", action="store_true")
ap = sub.add_parser("apply")
ap.add_argument("--no-restart", action="store_true")
sub.add_parser("list")
sh = sub.add_parser("show")
sh.add_argument("name")
ex = sub.add_parser("export")
ex.add_argument("name")
ex.add_argument("--endpoint", help="Override endpoint ip:port (default: auto-detect public ip)")
args = p.parse_args()
try:
if args.cmd == "gen-server":
gen_server(cfg, args.force)
elif args.cmd == "gen-client":
gen_client(cfg, args.name, args.force)
elif args.cmd == "add":
add_client(cfg, args.name, args.pubkey, args.ip, args.no_restart)
elif args.cmd == "del":
del_client(cfg, args.name, args.no_restart)
elif args.cmd == "apply":
generate_wg0_conf(cfg)
if not args.no_restart:
restart_wg()
elif args.cmd == "list":
cmd_list(cfg)
elif args.cmd == "show":
cmd_show(cfg, args.name)
elif args.cmd == "export":
export_client_conf(cfg, args.name, args.endpoint)
else:
p.print_help()
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()