FreeBSD: налаштування DragonFly Mail Agent для пошти root
0 (0)

8 Лютого 2026

В локальну пошту юзера root приходить багато листів від різних periodic задач.

Про perdiodic scripts трохи писав у FreeBSD: Home NAS, part 5 – ZFS pool, datasets, snapshots та моніторинг, тепер налаштувати пересилку пошти на зовнішню адресу.

Бо листи приходять кожного дня, а читати пошту локально незручно:

# mail -u root -H
Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/root": 58 messages 58 unread
...
 U 57 root@setevoy-nas      Sun Feb  8 03:19  46/1300  "setevoy-nas daily security run output"
 U 58 root@setevoy-nas      Sun Feb  8 03:19  99/3444  "setevoy-nas daily run output"

Аби їх отримувати на зовнішній ящик – додаємо Mail Transport Agent (MTA), який буде робити відправку на задану адресу.

Створення Google Mail App Passwords

Якщо включений 2FA – то використовуємо Google Mail App Passwords.

Пароль відобразить з пробілами – в конфігу DMA їх прибираємо:

Налаштування DragonFly Mail Agent

Документація – DragonFly Mail Agent (DMA) (FreeBSD Handbook) та DMA (Arch Linux Wiki).

Встановлюємо сам DMA:

# pkg install dma

Редагуємо файл /etc/dma/dma.conf:

SMARTHOST smtp.gmail.com
PORT 587

# SMTP authentication
AUTHPATH /etc/dma/auth.conf

SECURETRANSFER 
STARTTLS 

MASQUERADE [email protected]

Редагуємо /etc/dma/auth.conf – задаємо логін, хост і App password, який створили на початку (без пробілів):

[email protected]|smtp.gmail.com:mpd***sra

Задаємо права доступу – з групою mail:

# chown root:mail /etc/dma/auth.conf
chmod 640 /etc/dma/auth.conf

Перевіряємо відправку на звичайну адресу:

# echo "dma alias test" | mail [email protected]

І отримуємо листа:

Але якщо зараз відправити до юзера root, то лист не дійде з помилкою “The recipient address <root> is not a valid RFC 5321 address“.

Додаємо пошту рута до /etc/aliases:

...
root: [email protected]
...

Перевіряємо:

# echo "dma alias test" | mail root

І отримуємо листа “To: root“:

І для перевірки можна запустити виконання всіх periodic:

# periodic daily

І отримуємо листи:

Готово.

Loading

FreeBSD: Home NAS, part 10 – моніторинг з VictoriaMetrics та Grafana
0 (0)

7 Лютого 2026

Нарешті добрався до моніторингу.

Цікаво запустити стандартний стек з VictoriaMetrics + Grafana + Alertmanager не у звичному Kubernetes з Helm-чарту, а просто на хості.

Але підхід той самий, з яким моніторяться сервіси в AWS/Kubernetes – на FreeBSD буде VictoriaMetrics для метрик, Grafana для візуалізації, VMAlert та Alertmanager для алертів.

Хотя в моніторингу моїх EcoFlow зробив алерти через Grafana Alerts, перший раз їх пробував – непогано. Хоча все ж стандартний підхід, коли всі Alert Rules описані в файлах, мені заходить більше.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Оскільки це маленький домашній NAS, до якого доступ тільки в локальній мережі з через VPN – то буду робити без FreeBSD Jails. З ними, може, буду знайомитись ближче іншим разом, бо за всі роки користування FreeBSD (з… 2007 року? десь так) – жодного разу в jails нічого не крутив.

Поїхали.

Установка VictoriaMetrics

VictoriaMetrics є в портах FreeBSD і в репозиторії, хоча порти дещо відрізняються від звичної схеми – далі подивимось ці нюанси.

З репозиторію FreeBSD встановлюємо саму VictoriaMetrics:

root@setevoy-nas:~ # pkg install -y victoria-metrics

Перевіряємо що і куди встановило:

root@setevoy-nas:~ # pkg info -l victoria-metrics | grep -E 'bin|rc.d'
        /usr/local/bin/victoria-metrics
        /usr/local/etc/rc.d/victoria-metrics

У файлі /usr/local/etc/rc.d/victoria-metrics маємо список флагів, які можемо передати через /etc/rc.conf:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/victoria-metrics | grep victoria_metrics
# PROVIDE: victoria_metrics
name="victoria_metrics"
rcvar="victoria_metrics_enable"
logfile="${logdir}/victoria_metrics.log"
victoria_metrics_args=${victoria_metrics_args-"--storageDataPath=/var/db/victoria-metrics --retentionPeriod=1 --httpListenAddr=:8428"}
victoria_metrics_user="victoria-metrics"

Дані будуть зберігатись в /var/db/victoria-metrics, потім треба буде додати його в бекапи.

Додаємо запуск сервісу до /etc/rc.conf:

root@setevoy-nas:~ # sysrc victoria_metrics_enable="yes"
victoria_metrics_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service victoria-metrics start

Перевіряємо порти:

root@setevoy-nas:~ # netstat -an | grep 8428
tcp4       0      0 *.8428                 *.*                    LISTEN

Відкриваємо в браузері – все працює:

Установка node_exporter

Аби побачити в VictoriaMetrics якісь метрики – встановлюємо теж звичний багатьом node_exporter:

root@setevoy-nas:~ # pkg install -y node_exporter

Додаємо його запуск:

root@setevoy-nas:~ # sysrc node_exporter_enable="yes"
node_exporter_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service node_exporter start
Starting node_exporter.

Перевіряємо порт:

root@setevoy-nas:~ # netstat -an | grep 9100
tcp46      0      0 *.9100                 *.*                    LISTEN

Установка VMAgent

Отут вже трохи відмінностей, бо окремого FreeBSD port під VMAgent нема – але є загальний пакет vmutils, який встановлює відразу кілька компонентів:

root@setevoy-nas:~ # pkg install -y vmutils

Перевіряємо, що він нам додав:

root@setevoy-nas:~ # pkg info -l vmutils | grep bin
        /usr/local/bin/vmagent
        /usr/local/bin/vmalert
        /usr/local/bin/vmauth
        /usr/local/bin/vmbackup
        /usr/local/bin/vmctl
        /usr/local/bin/vmrestore

Але з vmutils встановлюється тільки один rc.d скрипт, для самого vmagent:

root@setevoy-nas:~ # pkg info -l vmutils | grep rc.d
        /usr/local/etc/rc.d/vmagent

Тому далі для VMAlert будемо писати власний.

Додаємо vmagent в rc.cconf:

root@setevoy-nas:~ # sysrc vmagent_enable="yes"
vmagent_enable:  -> yes

Поки не запускаємо – налаштуємо збір метрик з node_exporter.

Перевіряємо опції, з якими vmagent запускається – шукаємо дефолтний файл конфігу:

root@setevoy-nas:~ # cat /usr/local/etc/rc.d/vmagent
#!/bin/sh
...
vmagent_args=${vmagent_args-"--remoteWrite.tmpDataPath=/var/db/vmagent --promscrape.config=/usr/local/etc/prometheus/prometheus.yml --remoteWrite.url=http://127.0.0.1:8428/api/v1/write --memory.allowedPercent=80"}
...

Додаємо job="node_exporter" до /usr/local/etc/prometheus/prometheus.yml:

global:
  scrape_interval: 15s

scrape_configs:

  - job_name: vmagent
    scrape_interval: 60s
    scrape_timeout: 30s
    metrics_path: "/metrics"
    static_configs:
    - targets:
      - 127.0.0.1:8429
      labels:
        project: vmagent

  - job_name: "node_exporter"
    static_configs:
      - targets:
          - "127.0.0.1:9100"

Перевіряємо синтаксис:

root@setevoy-nas:~ # service vmagent checkconfig; echo $?
0

Запускаємо сервіс:

root@setevoy-nas:~ # service vmagent start

Перевіряємо в VMAgent /targets, http://nas.setevoy:8429/targets:

І метрики в VictoriaMetrics:

Установка Grafana

Теж встановлюємо із репозиторію:

root@setevoy-nas:~ # pkg install -y grafana

Конфіг-файл – /usr/local/etc/grafana/grafana.ini.

Додаємо запуск:

root@setevoy-nas:~ # sysrc grafana_enable="yes"
grafana_enable:  -> yes

Запускаємо:

root@setevoy-nas:~ # service grafana start
Starting grafana.

Для тестів можна взяти готовий дашборд – Node Exporter Full.

VictoriaMetrics Grafana data source на FreeBSD

Додаємо datasource:

Але відразу зловив помилку:

Хоча 100% плагін підписаний, бо не вперше ж його використовую в робочих проектах:

Пробував додати allow_loading_unsigned_plugins до /usr/local/etc/grafana/grafana.ini:

...
allow_loading_unsigned_plugins = victoriametrics-metrics-datasource
...

Або встановити з Grafaca CLI (перший раз не користувався):

root@setevoy-nas:~ # grafana cli plugins install victoriametrics-metrics-datasource
Grafana-server Init Failed: Could not find config defaults, make sure homepath command line parameter is set or working directory is homepat

Не допомогло.

Ну і потім вже глянув логи Grafana:

...
logger=installer.fs t=2026-02-06T17:09:29.946038823+02:00 level=info msg="Downloaded and extracted victoriametrics-metrics-datasource v0.21.0 zip successfully to /var/db/grafana/plugins/victoriametrics-metrics-datasource"
logger=plugins.backend.start t=2026-02-06T17:09:30.466419686+02:00 level=error msg="Could not start plugin backend" pluginId=victoriametrics-metrics-datasource error="fork/exec /var/db/grafana/plugins/victoriametrics-metrics-datasource/victoriametrics_metrics_backend_plugin_freebsd_amd64: no such file or directory"
...

“victoriametrics_metrics_backend_plugin_freebsd_amd64: no such file or directory”

Oh, c’mon…

Не став розбиратись далі – просто можемо використати стандартний плагін Prometheus (але пізніше розробників VictoriaMetrics потім за цю проблему спитаю).

Власне, якщо не зраджує пам’ять – то раніше, коли власного плагіну для Grafana у VictoriaMetrics не було, то ми і використовували дефолтний Prometheus, який йде в комплекті до Grafana:

Додаємо новий data source:

Називаємо його victoria-metrics, задаємо URL:

І тепер все працює:

Налаштування Alerting

I use Arch ntfy.sh BTW.

Дуже прикольний і простий сервіс. Має web, має мобільну апку.  Може напишу якось про нього окремо, бо просто в захваті.

Можна зробити алерти через Telegram – можливо, потім додам і його, але зараз ntfy.sh вистачить з головою.

Отже, VMlert у нас вже є – він рахує правила, які ми йому задаємо, і шле в Alertmanager.

Установка Alertmanager

Теж з репозиторію FreeBSD:

root@setevoy-nas:~ # pkg install -y alertmanager

Додаємо запуск:

root@setevoy-nas:~ # sysrc alertmanager_enable="yes"
alertmanager_enable:  -> yes

Видаляємо дефолтний файл, бо там багато зайвого:

root@setevoy-nas:~ # mv /usr/local/etc/alertmanager/alertmanager.yml /usr/local/etc/alertmanager/alertmanager.yml-default

Пишемо свій конфіг /usr/local/etc/alertmanager/alertmanager.yml:

global:
  resolve_timeout: 5m

route:
  receiver: "ntfy"
  group_by: ["alertname"]
  group_wait: 10s
  group_interval: 5m
  repeat_interval: 4h

receivers:
  - name: "ntfy"
    webhook_configs:
      - url: "https://ntfy.sh/setevoy-alertmanager-alerts"
        http_config:
          authorization:
            type: Bearer
            credentials: "tk_v9c***f2p"
        send_resolved: true

Запускаємо Alertmanager:

root@setevoy-nas:~ # service alertmanager restart

Перевіряємо його дашборд – http://nas.setevoy:9093/#/alerts:

Установка VMAlert

Бінарнік vmalert вже встановлено з пакету vmutils, але для нього нема скриптів для rc.d:

root@setevoy-nas:~ # pkg info -l vmutils | grep rc.d
        /usr/local/etc/rc.d/vmagent
root@setevoy-nas:~ # pkg info -l vmutils | grep bin
        /usr/local/bin/vmagent
        /usr/local/bin/vmalert
        /usr/local/bin/vmauth
        /usr/local/bin/vmbackup
        /usr/local/bin/vmctl
        /usr/local/bin/vmrestore

Тому пишемо власний /usr/local/etc/rc.d/vmalert – він простий, вайбокодиться без проблем:

#!/bin/sh

# PROVIDE: vmalert
# REQUIRE: LOGIN
# KEYWORD: shutdown

. /etc/rc.subr

name="vmalert"
rcvar="vmalert_enable"

load_rc_config $name

: ${vmalert_enable:="NO"}
: ${vmalert_user:="victoria-metrics"}
: ${vmalert_args:="--datasource.url=http://127.0.0.1:8428 --notifier.url=http://127.0.0.1:9093 --rule=/usr/local/etc/vmalert/*.yml"}

pidfile="/var/run/${name}.pid"
command="/usr/sbin/daemon"
procname="/usr/local/bin/vmalert"

command_args="-f -p ${pidfile} ${procname} ${vmalert_args}"

start_cmd="vmalert_start"
stop_cmd="vmalert_stop"

vmalert_start()
{
  echo "Starting vmalert"
  ${command} ${command_args}
}

vmalert_stop()
{
  echo "Stopping vmalert"
  kill `cat ${pidfile}`
}

run_rc_command "$1"

Задаємо права на запуск:

# chmod +x /usr/local/etc/rc.d/vmalert

Додаємо в запуск із системою:

root@setevoy-nas:~ # sysrc vmalert_enable="yes"
vmalert_enable:  -> yes

І запускаємо:

root@setevoy-nas:~ # service vmalert start

Перевіряємо на http://nas.setevoy:8880:

Додавання алертів

Власне, тепер можемо накидати алерти.

Створюємо файл /usr/local/etc/vmalert/node-alerts.yml:

root@setevoy-nas:~ # mkdir -p /usr/local/etc/vmalert/

Описуємо алерт:

groups:
  - name: node-exporter-alerts
    rules:
      - alert: NodeExporterDown
        expr: up{job="node_exporter"} == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "node_exporter down on {{ $labels.instance }}"
          description: "node_exporter is not reachable for more than 1 minute"

Перезапускаємо vmalert:

root@setevoy-nas:~ # service vmalert restart

Для тесту зупиняємо node_exporter:

root@setevoy-nas:~ # service node_exporter stop
Stopping node_exporter.
Waiting for PIDS: 1965.

І отримуємо алерт в VMAlert:

І повідомлення від ntfy.sh.

На телефоні:

Або в web:

Тюнинг Grafana dashboard та node_exporter Memory graphs

Дефолтна дашборда заточена під Linux, і на FreeBSD для правильного відображення графіків у Memory Basic треба трохи затюнити запити і метрики.

Перевіряємо наявні метрики по memory від node_exporter:

{__name__=~"node_memory_.*_bytes"}

Тут з основного маємо:

  • node_memory_size_bytes: total RAM
  • node_memory_free_bytes: реально вільна
  • node_memory_cache_bytes: filesystem cache (reclaimable)
  • node_memory_buffer_bytes: buffers (reclaimable)
  • node_memory_inactive_bytes: inactive pages (reclaimable)
  • node_memory_active_bytes: активно використовується
  • node_memory_wired_bytes: non-reclaimed памʼять (kernel, drivers)

Що нам тут цікаво – це загальна пам’ять, node_memory_free_bytes та node_memory_active_bytes.

Free RAM у FreeBSD – це дійсно free memory, тобто все поза кешами, inactive, wired, buffers і т.д.

Тому панельку з візуалізацією можемо побудувати з такими запитами:

  • загальна пам’ять
node_memory_size_bytes
  • used memory – те, що дійсно зайнято  і не може бути звільнено:
sum
(
    node_memory_active_bytes
    +
    node_memory_wired_bytes
)
  • вільна пам’ять – все поза всякими кешами-буферами і рештою зайнятої пам’яті:
node_memory_free_bytes{instance="$node",job="$job"}
  • swap used, хоча в мене його нема:
node_memory_swap_used_bytes{instance="$node",job="$job"}
  • відобразити % зайнятої памяті від загальної:
(
  node_memory_active_bytes
  + node_memory_wired_bytes
)
/
node_memory_size_bytes
* 100

В результаті маємо такі графіки для пам’яті:

А вся дашборда в мене поки виглядає так (це вже з доданим ZFS Exporter):

Окремо є Small варіант – для виводу на 7-ми дюймовий монітор, який буде стояти в серверній шафі:

Пізніше до дашборду вже пододаю ще корисних графіків і статусів.

Ну і для моніторингу системи і NAS буде корисно додати ще експортерів – smartctl_exporter, zfs_exporter тощо.

Loading

MikroTik: перше знайомство та Getting Started
0 (0)

22 Січня 2026

Давно подумував спробувати MikroTik, але все якось було ліньки розбиратись з RouterOS.

Нарешті, на хвилі зборки Home NAS проекту (див. початок в FreeBSD: Home NAS, part 1 – налаштування ZFS mirror) таки вирішив, що пора оновити і мережевий стек і замінити простенький TP-Link Archer на щось більш серйозне.

Так в мене з’явились два роутери MikroTik: RB4011iGS+RM – основний роутер, і MikroTik hAP ax3 – для WiFi.

До цього в мене були Linksys E4200 (2012-2020), потім з’явився Linksys EA6350 (2020-2024), і останнім був TP-Link Archer AX12 (2024-2025), і коли я перший раз відкрив MikroTik Web UI і подивився можливості… Це як пересісти з Запорожця на Мерседес.

Ну і нарешті – повноцінна консоль і SSH з коробки, а не через кастомні прошивки.

Можливостей в RouterOS дуже багато, тому одним постом по цій темі не обійтись, і в чернетках вже є кілька матеріалів, а почнемо з першого знайомства і початку роботи.

Архітектура моєї мережі

Перш ніж говорити про сам роутер – трохи про мій networking і ролі роутерів MikroTik.

Є дві мережі – “офіс” та дом, в обох “на вході” були TP-Link Archer AX12.

В “офісі” (в лапках, бо це просто сусідня квартира) стоїть ThnikCenter з FreeBSD/NAS, плюс робочий ноутбук і ігровий ПК. В основному всі девайси підключені до роутера кабелями, WiFi тільки для телефону і всяких EcoFlow, робота-пилососа тощо.

Вдома – пара ноутбуків, там вся мережа виключно WiFi.

Обидві мережі об’єднані з VPN, і по старій схемі TP-Link Archer в офісі мав port forwading до WireGuard на FreeBSD, а на FreeBSD були власне WireGuard як хаб та Unbound для локального DNS, плюс всякі Samba/NFS/etc.

Тепер жеж в офісі схема буде іншою:

  • MikroTik RB4011iGS:
    • на нього заходить кабель провайдера (оптика через ONU і далі по квартирі з Ethernet на роутер RB4011)
    • на ньому пізніше буде друге підключення ще одного LTE-роутера з SIM та мобільним інтернетом і автоматичним failover (див. старий пост Networking: коли немає світла – модем 4G ZTE + зовнішня антена: антена та сама, тільки роутер буде Teltonika RUT241)
    • WireGuard тепер буде тут
    • локальний DNS теж тепер буде тут
    • до RB4011iGS кабелями підключені ThinkCentre/NAS, робочий ноутбук та ігровий ПК
  • MikroTik hAP ax3: підключений кабелем до RB4011, пізніше переключу його в режим Access Point, поки стандартний WiFi роутер з власним NAT
  • TP-Link Archer AX12: підключений кабелем до RB4011, на ньому нічого не міняю, бо ліньки перепідключати різні домашні девайси типу дверного дзвоника, пожежної сигналізації, EcoFlow, etc

В домашній мережі не міняється нічого окрім налаштувань WireGuard на домашньому ноуті: раніше він через port-forwarding на офісному роутері підключався до FreeBSD, тепер буде ходити до RB4011.

І окремо – сервер самого rtfm.co.ua в DigitalOcean, який (буде) теж підключений з WireGuard до цієї мережі.

Загальна схема буде виглядати приблизно так:

Перше підключення до MikroTik

Боже, який це кайф – мати повноцінний SSH! Але про SSH трохи далі тут і потім ще окремим постом.

Взагалі MikroTik предоставляє кілька варіантів підключення:

Дефолтний юзер всюди admin. Паролі для MikroTik hAP ax3 зробили на висувному вкладиші (дуже прикольно):

А для RB4011 на паперовому Quick Guide.

Дефолтна мережа 192.168.88.0/24, адреса роутера, відповідно – 192.168.88.1.

WAN-порт на обох роутерах – перший, в нього втикаємо кабель провайдера, а ноутбук/ПК – в будь-який інший.

Для підключення до MikroTik hAP ax3 замість кабеля можна використати його дефолтну WiFi-мережу – теж дасть доступ до управління.

Web UI overview

Інтерфейс стандартний на обох роутерах – на RB4011 навіть є розділ “WiFi”, хоча в нього тільки Ethernet-порти.

Тут і далі в основному буду писати про RB4011, тому скріншоти будуть з нього.

Web має три “режими” – простий для Quick Setup:

Advanced – тут вже доступ до всіх можливостей:

І можна запустити Terminal прямо з Web:

В Design Skin можна вибрати які саме пункти меню будуть відображатись:

WinBox

При запуску утиліта сама сканує мережу і знаходить доступні для підключення девайси MikroTik:

При цьому підключитись можна як по IP, так і по MAC-адресі – на випадок, якщо зламали мережу.

А сам інтерфейс в принципі такий самий, як і в Web – стандартний для RouterOS:

І навіть є темна тема:

SSH

Тут все стандартно – просто з ноутбука/ПК виконуємо “ssh 192.168.88.1” (в мене вже переналаштований DHCP, тому адреса 192.168.0.1):

Можна використовувати ключі замість паролів, потім подивимось як.

Mobile client

І мобільна апка – підключаємось по IP:

Робота в консолі RouterOS

Вебом користуюсь мало, і тут і далі всі налаштування будуть з SSH.

Документація – Command Line Interface та Console.

Дуже цікава можливість Safe Mode: відкотить зміни, якщо зламали доступ і підключення розірвалось без коректного збереження налаштувань.

В RouteOS є повноцінна консоль, яка складається з ієрархічного дерева команд.

Наприклад, якщо в Web пункт меню IP => Firewall:

То в консолі це буде /ip firewall.

Є повноцінне автодоповнення по Tab:

Після переходу в меню можна з F1 подивитись доступні команди:

В документації говориться, що “?” має виводити підказку теж – але на 7+ версії це вже не працює (Reddit).

Замість “?” просто вибираємо команду, а потім F1 або Tab:

Getting started: перші налаштування

У MikroTik дуже класна документація (привіт, Confluence), і є власний розділ Getting started.

Пройдусь по основним речам, які робив на початку роботи.

Деякі скріни старі, тому ім’я хоста там буде “MikroTik” – дефолтне, далі глянемо, як це міняється.

IP теж може бути старий, дефолтний – 192.168.88.1. зараз він 192.168.0.1. Про налаштування DHCP – в наступному пості.

Backup та restore

У MirkoTik є два варіанти створення бекапу – з /export та /system backup.

/export створить текстовий файл з історією команд який можна прочитати – а /system backup створює бінарний файл, який включає в себе все, в тому числі ключі і сертифікати.

Але якщо конфіг переноситься на інший роутер – то system backup може сфейлитись, бо містить в собі прив’язку до конкретного девайсу, а результат з export – просто виконає команди.

/export та /import

Робимо /export в файл:

[setevoy@mikrotik-rb4011-gw] > /export file=init-backup

Тепер він є в Files:

І його можна прочитати.

З scp копіюємо собі на ноут:

[setevoy@setevoy-work ~] $ scp [email protected]:/init-backup.rsc .
[email protected]'s password: 
init-backup.rsc

І читаємо:

[setevoy@setevoy-work ~]  $ cat init-backup.rsc 
# 2026-01-22 15:21:51 by RouterOS 7.21
# software id = BUXG-TCU3
#
# model = RB4011iGS+
# serial number = HK50AXX5M2Y
/interface bridge
add admin-mac=04:F4:1C:89:8B:B3 auto-mac=no comment=defconf name=bridge
/interface wireguard
add listen-port=51820 mtu=1420 name=wg0
/interface list
add comment=defconf name=WAN
add comment=defconf name=LAN
/ip pool
add name=default-dhcp ranges=192.168.88.10-192.168.88.254
add name=vpn ranges=192.168.89.2-192.168.89.255
add name=dhcp_pool_lan ranges=192.168.0.50-192.168.0.200
...

Застосувати файл для відновлення параметрів:

/import file-name=init-backup.rsc

Система просто прочитає всі команди по черзі і виконає їх.

При цьому поточні налаштування не зміняться – якщо не відрізняють від того, що в файлі експорту, але можуть виникати дублі.

Експорт/імпорт не відновить:

  • паролі
  • сертифікати і приватні ключі
  • ліцензію
  • secrets (IPsec, WireGuard private keys)
  • деякі параметри /system

/system backup save та load

Для створення повного бекапу:

/system backup save name=before-change

Для відновлення:

/system backup load name=before-change

При цьому будуть видалені всі поточні налаштування – і відтворені з бекапу.

User management

Рекомендується створити власного юзера з root-правами, і відключити(але не видаляти) дефолтного admin.

Документація – User.

Першим ділом міняємо пароль admin:

[admin@mikrotik-rb4011-gw] > /user set admin password=PASSWORD

Прикольно, що після виконання строка з паролем відразу зникає з консолі.

Вивести всіх юзерів :

/user print

Активні сесії:

/user active print

Створюємо юзера, і задамо ліміт на адреси, з яких буде доступ (хоча ремоут SSH по дефолту і так недоступний на фаєрволі, про нього згодом):

/user add name=setevoy group=full password=PASSWORD address=192.168.0.0/24,192.168.88.0/24

Перевіряємо – або для всіх з /user print detail, або конкретного з where:

/user print detail where name="setevoy"

Змінити пароль або інші атрибути:

/user set [find name="setevoy"] password=NEW_PASSWORD

Або по ID – знаходимо ID з /user print:

І використовуємо його для /user set:

/user set 1 password=NEW_PASSWORD

Підключаємось під новим юзером:

[setevoy@setevoy-work ~]  $ ssh 192.168.0.1
...
[email protected]'s password: 
...
[setevoy@mikrotik-rb4011-gw] >

Апгрейд роутера

Бекапимось 🙂

Хоча завжди можна резетнути до заводських налаштувань, але краще завести звичку створити бекап.

Апгрейд включає в себе два окремих процеси – оновлення RouterOS та апдейти для firmware.

RouterOS upgrade

Документація – Upgrading and installation.

Перевіряємо поточну версію системи:

/system package print

Результат:

Columns: NAME, VERSION, BUILD-TIME, SIZE
# NAME      VERSION  BUILD-TIME           SIZE   
0 routeros  7.18.2   2025-03-11 11:59:04  11.5MiB

Перевіряємо наявність апдейтів:

/system package update check-for-updates

Результат:

[setevoy@MikroTik] > /system package update check-for-updates
            channel: stable                  
  installed-version: 7.18.2                  
     latest-version: 7.21                    
             status: New version is available

Завантажуємо – це тільки загрузка:

/system package update download

Результат:

[setevoy@MikroTik] > /system package update download
            channel: stable                                        
  installed-version: 7.18.2                                        
     latest-version: 7.21                                          
             status: Downloaded, please reboot router to upgrade it

І запускаємо вже сам процес апгрейду:

/system package update install

Система піде в ребут:

[setevoy@MikroTik] > /system package update install
            channel: stable                      
  installed-version: 7.18.2                      
     latest-version: 7.21                        
             status: calculating download size...
Received disconnect from 192.168.88.1 port 22:11: shutdown/reboot
Disconnected from 192.168.88.1 port 22

RouterBOARD (firmware) upgrade

Документація – RouterBOARD.

Перевіряємо поточну версію:

/system routerboard print

В мене це виглядало так:

[setevoy@MikroTik] > /system routerboard print
       routerboard: yes        
             model: RB4011iGS+ 
          revision: r2         
     serial-number: HK50AXX5M2Y
     firmware-type: al2        
  factory-firmware: 7.18.2     
  current-firmware: 7.18.2     
  upgrade-firmware: 7.21

Встановлена 7.18.2, є апгрейд до 7.21.

Запускаємо апгрейд:

/system routerboard upgrade

Результат:

[setevoy@MikroTik] > /system routerboard upgrade
Do you really want to upgrade firmware? [y/n] 
y
[setevoy@MikroTik] > 
14:13:58 echo: system,info,critical Firmware upgraded successfully, please reboot for changes to take effect!

Ребутаємо роутер:

[setevoy@MikroTik] > /system reboot
Reboot, yes? [y/N]: 
y
system will reboot shortly
Connection to 192.168.88.1 closed.

Перевіряємо ще раз:

[setevoy@MikroTik] > /system routerboard print  
       routerboard: yes        
             model: RB4011iGS+ 
          revision: r2         
     serial-number: HK50AXX5M2Y
     firmware-type: al2        
  factory-firmware: 7.18.2     
  current-firmware: 7.21       
  upgrade-firmware: 7.21

System management: основні команди

Корисні команди для роботи з системою.

Вивести події з логу:

/log print

Або з фільтром:

/log print where topics~"error|warning"

Вивести стан системи, версію, аптайм:

/system resource print

Коректно вимкнути систему:

/system shutdown

Перевірка живлення, температури:

/system health print

Навантаження CPU:

/tool profile

Cтан інтерфейсів коротко:

/interface print

Або детально:

/interface print detail

Адреси:

/ip address print

Роути:

/ip route print

Distance тут – пріорітет: можна мати друге інтернет-підключення (як я планую – на ethernet port 2 підключити LTE-роутер з SIM-картою), задати йому Distance == 2, і тоді трафік буде йти через перший порт – якщо він доступний, а якщо ні – то через другий.

Інформація по DNS:

/ip dns print

Виконати ping на якийсь хост:

/ping 8.8.8.8 src-address=192.168.0.1

Або traceroute (динамічний, як mtr на Linux/FreeBSD):

/tool traceroute 8.8.8.8

Коректно перезавантажити або виключити:

/system reboot
/system shutdown

Задати ім’я хоста:

/system identity set name=mikrotik-rb4011-gw

Власне, на цьому для початку все.

Що далі? Next steps

Про що ще думаю писати – частина вже є в чернетках, частину буду (якщо буде час) писати з нуля:

  1. налаштування DHCP
  2. налаштування DNS
  3. SSH і firewall – юзери, аутентифікація по ключам, правила фаєрволу
  4. налаштування WireGuard для підключення Peers
  5. scripts, alerting, monitoring – дуже класна можливість писати скрипти, які можуть слати алерти, див. Scripting
  6. резервний канал інтернету через LTE-роутер
  7. WiFi tuning

Loading

FreeBSD: Home NAS, part 9 – backup даних з rclone до AWS S3 та Google Drive
0 (0)

21 Січня 2026

В попередньому пості серії по налаштуванню Home NAS на FreeBSD знайомились з Restic – утилітою для роботи з бекапами, і яка підтримує шифрування, снапшоти, історію змін, див. FreeBSD: Home NAS, part 8 – backup даних NFS та Samba з restic.

Але окрім архівних даних в S3 хочеться мати “offsite hot copy” в Google Drive та AWS S3, аби мати доступ до даних постійно, і які не треба відновлювати із бекапу, а можна просто скопіювати з CLI або навіть з браузера.

При цьому не хочеться розводити зоопарк різних систем, а працювати з одною, яка буде вміти підключатись і до AWS, і до Google Drive.

Власне, під час пошуку того, як з restic копіювати дані в Google Drive знайшов такий собі “швейцарський ніж” – Rclone.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

rclone overview

Rclone (“rsync for cloud storage“) – CLI-утиліта, вміє працювати просто з безліччю різних бекендів – і локальними даними або NFS, Samba, і FTP, і WebDAV, і, звісно, AWS S3 та Google Drive, див. всі в Overview of cloud storage systems.

Основні плюшки системи:

  • написаний на Go
  • можливість одною CLI отримати доступ до даних в Google Drive та S3
  • client-side шифрування даних і імен файлів
  • режими copy та sync, аналогічні rsync
  • є можливість змонтувати ремоут в локальну директорію, і працювати як зі звичайною папкою з даними (див. rclone mount)
  • вміє працювати як “проксі” між двома remotes (наприклад, копіювати дані між Google Drive та S3)
  • має Web GUI

Але при цьому rclone не є саме бекап-системою – не використовує снапшоти, не веде історію змін даних, не відновлює стан “на дату”.

rclone та Google Drive backend

Почнемо з Google Drive, бо це основне, для чого я планую використовувати rclone, але далі налаштуємо і AWS S3.

Документація – Google Drive.

Створення Google API keys для rclone

Для роботи з Google Drive створимо API keys, і цей процес опишу окремо, бо створення ключів в Google трохи заплутане, і я кожного разу шукаю гайд як це робити.

Переходимо в Google API Console, вибираємо існуючий або створюємо новий проект:

Зліва вибираємо “Enabled APIs & services”, клікаємо “Enable APIs”:

В пошуку знаходимо “Google Drive API”:

Вмикаємо його:

 

Переходимо в “OAuth consent screen”:

Переходимо в “Branding”, заповнюємо “App information” – задаємо ім’я, це чисто для нас, вказуємо email:

І внизу в “Developer contact information” ще раз пошту:

 

Зберігаємо, переходимо в “Audience”, перевіряємо, що тут “User type” заданий як External:

Переходимо в “Clients”, потім “Create client”:

Вказуємо “Application type” як Desktop app:

Отримуємо Client ID та Client Secret, зберігаємо собі:

Переходимо до налаштувань підключень вже в самому rclone.

Налаштування Google Drive remote

Виконуємо rclone config, вибираємо “n) New remote“, задаємо ім’я:

...
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-google-drive
...

Далі вибираємо з яким бекендом rclone буде працювати.

Для Google Drive це 22 (можна вказати номер, можна ім’я “drive“):

...
22 / Google Drive
   \ (drive)
...

Наступний крок аутентифікація – задаємо ключі:

...
Option client_id.
Google Application Client Id
...
Enter a value. Press Enter to leave empty.
client_id> 377***7i7.apps.googleusercontent.com

Option client_secret.
OAuth Client Secret.
Leave blank normally.
Enter a value. Press Enter to leave empty.
client_secret> GOC***gjX
...

Задаємо рівень доступу – тут можна дати або повний доступ до всього драйву, або, якщо rclone буде тільки для бекапів, то вибрати “Access to files created by rclone only”.

На ноутбуках можна задати повний доступ, а на FreeBSD зробимо “тільки для свої файлів”:

В Advanced можна редагувати параметри типу “use_trash” та “Upload chunk size”, але це можна зробити пізніше – зараз просто тиснемо Enter.

Наступний крок – аутентифікація для отримання токену від Google:

...
Use web browser to automatically authenticate rclone with remote?
 * Say Y if the machine running rclone has a web browser you can use
 * Say N if running rclone on a (remote) machine without web browser access
If not sure try Y. If Y failed, try N.

y) Yes (default)
n) No

Так як це робиться на FreeBSD без браузеру, то вибираємо No – rclone згенерує токен, який треба вказати на машині з браузером, де є інший інстанс rclone:

...
y/n> n

Option config_token.
For this to work, you will need rclone available on a machine that has
a web browser available.
For more help and alternate methods see: https://rclone.org/remote_setup/
Execute the following on the machine with the web browser (same rclone
version recommended):
        rclone authorize "drive" "eyJ***ifQ"
Then paste the result.
Enter a value.

Виконуємо на ноуті:

$ rclone authorize "drive" "eyJ***ifQ"
2026/01/07 16:35:38 NOTICE: Make sure your Redirect URL is set to "http://127.0.0.1:53682/" in your custom config.
2026/01/07 16:35:38 NOTICE: If your browser doesn't open automatically go to the following link: http://127.0.0.1:53682/auth?state=Lf9q_HUVFlBUc2UqlSUqpw
2026/01/07 16:35:38 NOTICE: Log in and authorize rclone for access
2026/01/07 16:35:38 NOTICE: Waiting for code...

Відкривається браузер, вибираємо акаунт:

Дозволяємо доступ:

Отримуємо Success:

А на ноут в консолі, з якої викликали rclone authorize прийде токен:

...
2026/01/07 16:35:38 NOTICE: Waiting for code...
2026/01/07 16:37:33 NOTICE: Got code
Paste the following into your remote machine --->
eyJ...ifQ
<---End paste

Копіюємо його до rclone config на FreeBSD хості:

...
Enter a value.
config_token> eyJ***ifQ
...

І нове підключення готове:

...
Configuration complete.
Options:
- type: drive
- client_id: 377***7i7.apps.googleusercontent.com
- client_secret: GOC***gjX
- scope: drive.file
- token: {"access_token":"ya2***","expires_in":3599}
- team_drive: 
Keep this "nas-google-drive" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote

Глянути всі налаштовані бекенди можна з rclone listremotes:

root@setevoy-nas:/home/setevoy # rclone listremotes
nas-google-drive:

Або повна інформація з rclone config show:

root@setevoy-nas:/home/setevoy # rclone config show
[nas-google-drive]
type = drive
client_id = ***7i7.apps.googleusercontent.com
client_secret = GOCSPX-***gjX
scope = drive.file
token = {"access_token":"???"expires_in":3599}
team_drive =

scope = drive.file тут як раз вказує доступ тільки до даних від самого rclone.

Перевіряємо доступ до диску – свторюємо каталог:

root@setevoy-nas:~ # rclone mkdir nas-google-drive:Backups/Rclone

Перевіряємо зміст з rclone lsd та -R (recursive):

root@setevoy-nas:~ # rclone lsd -R nas-google-drive:Backups
           0 2026-01-20 16:04:10        -1 Rclone

Тепер налаштуємо ще AWS S3, а далі подивимось на основні команди для роботи з rclone.

rclone та AWS S3 backend

Документація – Amazon S3 Storage Providers.

Якби rclone був десь в AWS на EC2 або в EKS – то можна було б використати IAM Role, зараз просто зробимо з ключами.

Створення AWS IAM Policy та IAM User

Краще, звісно, зробити окремого юзера з власною політикою, у якого буде доступ до конкретної корзини, а не всього аккаунту.

Створюємо бакет:

Створюємо IAM Policy з повним доступом тільки до цього бакету:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RcloneNasBackupsFullAccess",
      "Effect": "Allow",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::setevoy-backups-nas",
        "arn:aws:s3:::setevoy-backups-nas/*"
      ]
    }
  ]
}

Зберігаємо:

Створюємо юзера – без доступу до AWS Management Console:

Підключаємо створену вище політику:

Зберігаємо юзера:

Створюємо для нього ключі доступу:

Вибираємо Application running outside AWS:

Зберігаємо ключі:

Налаштування AWS S3 remote

Запускаємо rclone config, вибираємо “New remote”, задаємо ім’я:

root@setevoy-nas:~ # rclone config
Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-s3-setevoy-backups
...

Далі тип – вибираємо 4 (s3), потім 1 – AWS:

...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
 1 / 1Fichier
   \ (fichier)
 2 / Akamai NetStorage
   \ (netstorage)
 3 / Alias for an existing remote
   \ (alias)
 4 / Amazon S3 Compliant Storage Providers including AWS, Alibaba, ArvanCloud, Ceph, ChinaMobile, Cloudflare, DigitalOcean, Dreamhost, Exaba, FlashBlade, GCS, HuaweiOBS, IBMCOS, IDrive, IONOS, LyveCloud, Leviia, Liara, Linode, Magalu, Mega, Minio, Netease, Outscale, OVHcloud, Petabox, RackCorp, Rclone, Scaleway, SeaweedFS, Selectel, StackPath, Storj, Synology, TencentCOS, Wasabi, Qiniu, Zata and others
   \ (s3)
...
Storage> s3

Option provider.
Choose your S3 provider.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Amazon Web Services (AWS) S3
   \ (AWS)
...

Далі вибираємо “Enter AWS credentials in the next step” і задаємо ключі:

...
Option env_auth.
Get AWS credentials from runtime (environment variables or EC2/ECS meta data if no env vars).
Only applies if access_key_id and secret_access_key is blank.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (false).
 1 / Enter AWS credentials in the next step.
   \ (false)
 2 / Get AWS credentials from the environment (env vars or IAM).
   \ (true)
env_auth> 1

Option access_key_id.
AWS Access Key ID.
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
access_key_id> AKI***VXZ

Option secret_access_key.
AWS Secret Access Key (password).
Leave blank for anonymous access or runtime credentials.
Enter a value. Press Enter to leave empty.
secret_access_key> MkP***xJ/
...

Далі регіон бакету – він є в Properties:

Задаємо його:

...
Option region.
Region to connect to.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
   / The default endpoint - a good choice if you are unsure.
 1 | US Region, Northern Virginia, or Pacific Northwest.
   | Leave location constraint empty.
   \ (us-east-1)
...
region> eu-west-1
...

Option endpoint залишаємо без мін, в Option location_constraint ще раз задаємо “eu-west-1“:

Option location_constraint.
Location constraint - must be set to match the Region.
Used when creating buckets only.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Empty for US Region, Northern Virginia, or Pacific Northwest
   \ ()
...
 6 / EU (Ireland) Region
   \ (eu-west-1)
...
location_constraint> eu-west-1

Option acl можна пропустити – у нас окрема корзина, в якій вже є всі налаштування ACL.

В server_side_encryption вибираємо “AES256“, далі “None“:

...
Option server_side_encryption.
The server-side encryption algorithm used when storing this object in S3.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / None
   \ ()
 2 / AES256
   \ (AES256)
 3 / aws:kms
   \ (aws:kms)
server_side_encryption> 2

Option sse_kms_key_id.
If using KMS ID you must provide the ARN of Key.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / None
   \ ()
 2 / arn:aws:kms:*
   \ (arn:aws:kms:us-east-1:*)
sse_kms_key_id> 1

Далі тип Storage Classes, див The Ultimate Guide to AWS S3 Pricing in 2026.

Можна взяти INTELLIGENT_TIERING:

...
Option storage_class.
The storage class to use when storing new objects in S3.
Choose a number from below, or type in your own value.
Press Enter to leave empty.
 1 / Default
   \ ()
...
 8 / Intelligent-Tiering storage class
   \ (INTELLIGENT_TIERING)
...
storage_class> 8

Зберігаємо – маємо новий бекенд:

...

Edit advanced config?
y) Yes
n) No (default)
y/n> 

Configuration complete.
Options:
- type: s3
- provider: AWS
- access_key_id: AKI***VXZ
- secret_access_key: MkP***zxJ/
- region: eu-west-1
- location_constraint: eu-west-1
- server_side_encryption: AES256
- storage_class: INTELLIGENT_TIERING
Keep this "nas-s3-setevoy-backups" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> 

Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-s3-setevoy-backups s3
...

Для роботи з S3 бакетом використовуємо формат remote_name:backet_name.

Створюємо в бакеті файл healthcheck.txt і каталог test – використовуємо rclone rcat:

root@setevoy-nas:~ # echo test | rclone rcat nas-s3-setevoy-backups:setevoy-backups-nas/test/healthcheck.txt

Перевіряємо зміст корзини з rclone ls:

root@setevoy-nas:~ # rclone ls nas-s3-setevoy-backups:setevoy-backups-nas/test
        5 healthcheck.txt

Rclone та шифрування

Задля більшої безпеки rclone може шифрувати свій локальний конфігураційний файл, а для безпечного зберігання даних в remote backends – може шифрувати дані там.

Rclone remote Crypt backend

Документація – Crypt.

crypt створюється як окремий бекенд, але використовує вже існуючий.

Наприклад, маючи nas-google-drive можна створити новий storage backend nas-google-drive-crypted і використовувати його: він буде таким собі “проксі” – ми пишемо дані “в нього”, він виконує шифрування, а потім “під капотом”, аби записати файли в Google Drive, використовує “оригінальний” бекенд nas-google-drive.

Створюємо новий remote:

root@setevoy-nas:~ # rclone config
Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-s3-setevoy-backups s3

e) Edit existing remote
n) New remote
d) Delete remote
r) Rename remote
c) Copy remote
s) Set configuration password
q) Quit config
e/n/d/r/c/s/q> n

Enter name for new remote.
name> nas-google-drive-crypted

В типі вибираємо “15 – Encrypt/Decrypt a remote”:

...
Option Storage.
Type of storage to configure.
Choose a number from below, or type in your own value.
...
15 / Encrypt/Decrypt a remote
   \ (crypt)
...
Storage> crypt

Далі вказуємо “source backend”, в якому будуть зашифровані дані.

Тут важливий момент: можна вказати або весь storage як nas-google-drive:Backups/Rclone (корінь для rclone в моєму випадку) – або створити в ньому окремий каталог, в якому будуть зберігатись зашифровані дані:

...
Option remote.
Remote to encrypt/decrypt.
Normally should contain a ':' and a path, e.g. "myremote:path/to/dir",
"myremote:bucket" or maybe "myremote:" (not recommended).
Enter a value.
remote> nas-google-drive:Backups/Rclone/Vault

Далі є варіанти – шифрувати імена файлів і каталогів чи ні, дефолт – шифрувати:

...
Option filename_encryption.
How to encrypt the filenames.
Choose a number from below, or type in your own value of type string.
Press Enter for the default (standard).
   / Encrypt the filenames.
 1 | See the docs for the details.
   \ (standard)
 2 / Very simple filename obfuscation.
   \ (obfuscate)
   / Don't encrypt the file names.
 3 | Adds a ".bin", or "suffix" extension only.
   \ (off)
filename_encryption> 

Option directory_name_encryption.
Option to either encrypt directory names or leave them intact.
NB If filename_encryption is "off" then this option will do nothing.
Choose a number from below, or type in your own boolean value (true or false).
Press Enter for the default (true).
 1 / Encrypt directory names.
   \ (true)
 2 / Don't encrypt directory names, leave them intact.
   \ (false)
directory_name_encryption> 

І останнє – вказати пароль і опціонально salt:

...
Option password.
Password or pass phrase for encryption.
Choose an alternative below.
y) Yes, type in my own password
g) Generate random password
y/g> y
Enter the password:
password:
Confirm the password:
password:

Option password2.
Password or pass phrase for salt.
Optional but recommended.
Should be different to the previous password.
Choose an alternative below. Press Enter for the default (n).
y) Yes, type in my own password
g) Generate random password
n) No, leave this optional password blank (default)
y/g/n>

Готово:

...
Configuration complete.
Options:
- type: crypt
- remote: nas-google-drive:Backups/Rclone/Vault
- password: *** ENCRYPTED ***
Keep this "nas-google-drive-crypted" remote?
y) Yes this is OK (default)
e) Edit this remote
d) Delete this remote
y/e/d> 

Current remotes:

Name                 Type
====                 ====
nas-google-drive     drive
nas-google-drive-crypted crypt
nas-s3-setevoy-backups s3

Тепер, якщо скопіюємо туди текстовий файл, то прочитати його зможемо тільки з rclone:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive-crypted:

Якщо глянути його в Google Drive, то побачимо щось таке:

Якщо просто напряму просто завантажити собі на ноутбук із Google Drive Web UI, то дані теж недоступні:

[setevoy@setevoy-work ~]  $ file Temp/56ncq9f6nnvup446abn28tno20 
Temp/56ncq9f6nnvup446abn28tno20: data

[setevoy@setevoy-work ~]  $ cat Temp/56ncq9f6nnvup446abn28tno20 
����~���ܻv� ���ڰ�~qz
����zG�4nNEQ

Але все нормально читається з rclone:

root@setevoy-nas:~ # rclone cat nas-google-drive-crypted:rclone-copy.txt
test

І можна відновити собі локально з rclone copy або rclone copyto:

root@setevoy-nas:~ # rclone copyto nas-google-drive-crypted:rclone-copy.txt /home/setevoy/decrypted-rclone-copy.txt

root@setevoy-nas:~ # cat /home/setevoy/decrypted-rclone-copy.txt
test

rclone та config file ecnryption

rclone зберігає всі свої налаштування в файлі ~/.config/rclone/rclone.conf, і по дефолту файл не шифрується:

root@setevoy-nas:~ # cat ~/.config/rclone/rclone.conf
[nas-google-drive]
type = drive
client_id = ***.apps.googleusercontent.com
client_secret = GOCSPX-***
scope = drive.file
token = {"access_token":***","expiry":"2026-01-17T18:09:01.116266823+02:00","expires_in":3599}
team_drive =

Але можна задати пароль:

root@setevoy-nas:~ # rclone config encryption set
Enter NEW configuration password:
password:
Confirm NEW configuration password:
password:

Тепер файл зашифровано:

root@setevoy-nas:~ # cat ~/.config/rclone/rclone.conf
# Encrypted rclone configuration File

RCLONE_ENCRYPT_V0:
g3M***LSA=

А при роботі з rclone він буде просити ввести пароль для читання конфігу:

root@setevoy-nas:~ # rclone config show
Enter configuration password:

Для автоматизації в скриптах або cron пароль можна передати через змінну RCLONE_CONFIG_PASS.

Див. rclone config encryption та rclone config show.

Основні можливості і команди

Головні команди, принаймні при використанні rclone для бекапів, це rclone copy та rclone sync.

З найбільш цікавих команд варто глянути такі:

  • rclone bisync: повністю двосторонньо синхронізує src та dst (копіює і видаляє)
  • rclone cat та rclone rcat: читання з remote у stdout або запис у remote зі stdout
  • rclone delete та rclone deletefile: видалення даних з можливістю використання фільтрів (--exclude/--exclude)
  • rclone ls та rclone lsd: ls – список файлів з розмірами, lsd – список директорій
  • rclone mkdir: створення директорії на remote
  • rclone mount: монтує remote як файлову систему (FUSE)
  • rclone move: переносить файли (copy + delete з src)
  • rclone ncdu: інтерактивний перегляд використання місця на remote
  • rclone rmdir: видалення порожньої директорії
  • rclone rmdirs: рекурсивне видалення порожніх директорій
  • rclone purge: повне видалення каталогу з усім вмістом
  • rclone size: показує кількість файлів і загальний розмір
  • rclone test speed: тест швидкості upload/download до remote
  • rclone tree: показує дерево каталогів

Корисні Flags

Див. всі в Global Flags.

Найбільш цікаві:

  • --check-first: виконати перевірку даних між src та dst до початку копіювання
  • --checksum: порівнювати файл між src та dst не за size+mtime, а по MD5SUM checksum – повільніше, але точніше, корисно для критичних даних
  • --immutable: не змінювати файл в dst, якщо він відрізняється від src, а впасти з помилкою
  • --interactive: підтвердження змін вручну
  • --dry-run: тестове виконання без копіювання
  • --progress: відобразити процес
  • --transfers N: кількість файлів, які копіюються одночасно (дефолт 4)
  • --create-empty-src-dirs: якщо src каталог порожній – то створити порожній каталог в dst (не працює з S3)
  • --exclude та --exclude-from, --include та --include-from: список або файл зі списком даних, які треба включити або виключити з копіювання, див. Filter
  • --log-file: куди писати лог (корисно для автоматизації)
  • --fast-list: створює один великий список директорій і файлів, який тримає в пам’яті, а не для кожної директорії окремо (більше пам’яті – але швидше і менше API-запитів до dst)
  • --update: пропустити файли, modification time яких на dst новіший, ніж в src
  • --human-readable: використовувати формат Ki/Mi/Gi

Використання rclone copy та rclone copyto

rclone copy просто копіює файл або директорію до заданого remote.

Якщо src – каталог з підкаталогами, то будуть скопійовані всі дані і збережено структуру каталогів.

Наприклад, маємо локально каталоги з файлами:

root@setevoy-nas:~ # tree /tmp/new/
/tmp/new/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

Запускаємо rclone copy:

root@setevoy-nas:~ # rclone copy /tmp/new/ nas-google-drive:Backups/Rclone

Перевіряємо на remote:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/Rclone/
/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

Аналогічно, можемо просто скопіювати окремий файл в новий каталог:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir

І тепер:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir/rclone-copy.txt
        2 another/dir/a.txt
        2 another/dir/sub/b.txt

Але з rclone copy не можна вказати нове ім’я файлу, тобто:

root@setevoy-nas:~ # rclone copy /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir/new-rclone-copy.txt

Створить нову директорію, а не файл з ім’ям new-rclone-copy.txt:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir/rclone-copy.txt
        5 rclone-dir/new-rclone-copy.txt/rclone-copy.txt

Для копіювання з новим іменем використовуємо rclone copyto:

root@setevoy-nas:~ # rclone copyto /root/rclone-copy.txt nas-google-drive:Backups/Rclone/rclone-dir-copyto/new-rclone-copyto.txt

В результаті:

root@setevoy-nas:~ # rclone ls nas-google-drive:Backups/Rclone
        5 rclone-dir-copyto/new-rclone-copyto.txt
...

Використання rclone sync

rclone sync виконує повну синхронізацію між src та dst: якщо в src файл було видалено – він видалиться і на dst. Див. також rclone bisync.

Корисні флаги тут:

  • --backup-dir: на dst не видаляти файл, який змінився в src, а зберегти в окрему директорію
  • --delete-after та --delete-before: видаляти дані до або після успішного копіювання
  • --suffix: додати суфікс до даних, які змінились

З rclone purge видаляємо створені під час тестів вище дані (видалить і саму директорію):

root@setevoy-nas:~ # rclone purge nas-google-drive:Backups/Rclone/
root@setevoy-nas:~ # rclone mkdir nas-google-drive:Backups/Rclone/

Виконуємо rclone sync:

root@setevoy-nas:~ # rclone sync /tmp/new/ nas-google-drive:Backups/Rclone/

Отримуємо аналогічну структуру на remote:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/Rclone/
/
└── another
    └── dir
        ├── a.txt
        └── sub
            └── b.txt

І приклад того, як працює --backup-dir.

Змінимо зміст файлу в src:

root@setevoy-nas:~ # echo updated > /tmp/new/another/dir/a.txt

Виконуємо rclone sync і вказуємо --backup-dir:

root@setevoy-nas:~ # rclone sync /tmp/new/ nas-google-drive:Backups/Rclone --backup-dir nas-google-drive:Backups/Rclone-changed/$(date +%Y-%m-%d-%H-%M-%S)

Але --backup-dir має бути поза dst – тобто не можна робити rclone sync /path/src/ nas-google-drive:path/dst/ --backup-dir path/dst/backupDir.

Тепер у нас в корні Backups/ є новий каталог Rclone-changed/:

root@setevoy-nas:~ # rclone tree nas-google-drive:Backups/
/
├── Rclone
│   └── another
│       └── dir
│           ├── a.txt
│           └── sub
│               └── b.txt
└── Rclone-changed
    └── 2026-01-21-12-08-33
        └── another
            └── dir
                └── a.txt

В якому збережено оригінальну копію файлу a.txt:

root@setevoy-nas:~ # rclone cat nas-google-drive:Backups/Rclone-changed/2026-01-21-12-08-33/another/dir/a.txt
a

А в Backups/Rclone/another/dir/a.txt у нас оновлений файл:

root@setevoy-nas:~ # rclone cat nas-google-drive:Backups/Rclone/another/dir/a.txt
updated

Ну і на цьому, мабуть, все.

Тепер можна створити кілька cron і налаштувати бекап бекапів.

Loading

Arch Linux: “містичні” таймаути з DNS та “в пошуках Ethernet-істини”
0 (0)

19 Січня 2026

Вже пару місяців, як на робочому ноуті Lenovo ThinkPad T14 Gen 5 з Arch Linux виникла проблема з відкриттям нових сайтів – перші 10-15 секунд сайт завантажується “шматочками”, наприклад:

Але потім “розчехляється”, і все починає працювати чудово:

Нарешті, як почав налаштовувати нормальну домашню мережу з VPN (див. FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer та routing), а потім для неї – DNS (див. FreeBSD: Home NAS, part 4 – локальний DNS з Unbound), то дійшли руки розібратись і з цієї проблемою.

І проблема виявилась дуже цікавою. Причину шукав довго, і перепровірив купу різних налаштувань – від IPv6 і DNS до драйверу мережової карти.

Головне, що проблема не те щоб була критичною – в цілому інет працював, а тому я іноді починав шукати причину, потім закидував, потім знов повертався.

The issue: “communications error to 192.168.0.1#53: timed out”

Що цікаво, що проблема спостерігалась тільки на Ethernet-підключені – на WiFi все працювало чудово.

А на Ethernet репродьюсилось на різних кабелях і через різні роутери.

Значить – що? Значить – або Сєня щось наковиряв руками у своєму Linux, або десь колись прилетів “кривий” апдейт чи до ядра, чи до драйвера, чи до якось бібліотеки.

Вже не пам’ятаю чому, але спершу грішив на DNS, бо ми ж знаємо, що:

І такі да – під час спроб зарепродьюсити це вдалось саме з DNS, під час тестів з dig – тому довго копав в цю сторону.

Виглядала проблема так: робимо dig, 10-15 запитів проходять нормально, а потім прилітає “communications error to 192.168.0.1#53: timed out“:

$ time dig google.com +short @192.168.0.1
;; communications error to 192.168.0.1#53: timed out
...

real    0m5.018s
user    0m0.004s
sys     0m0.008s

Ну і це виглядало, як дійсно причина того, що сайти тупили з загрузкою контенту: якщо DNS періодично відвалюється, а сайти мають купу додаткових скриптів і картинок, які підвантажуються з інших ресурсів – то поки всі хости разрезолвляться, поки отримаємо всі адреси, поки почнеться завантаження – як раз маємо цю затримку в кількадесят секунд.

Логічно? Так.

Тому і всі подальше тести я робив вже в циклі з dig:

$ for i in {1..50}; do { time dig +nocookie +noedns +tries=1 +time=2 google.com >/dev/null; } 2>&1; done
...
real    0m0.016s
...
real    0m2.015s
...
real    0m0.013s
...
real    0m1.392s

І такий результат був постійно – пачка запитів проходить нормально – “real 0m0.016s“, а потім на якомусь одному – таймаут і “real 0m2.015s” (бо +time=2 – чекати 2 секунди, а не дефолтні 5).

Ця ж проблема була видна з tcpdump: в 09:57:47 запит відправлений, але відповіді не отримано, через 2 секунди, в 09:57:49 – новий запит, і на нього вже відповідь прийшла:

...
09:57:47.717951 IP setevoy-work.40923 > _gateway.domain: 13058+ [1au] A? google.com. (51)
09:57:49.729589 IP setevoy-work.45441 > _gateway.domain: 63641+ [1au] A? google.com. (51)
09:57:49.730249 IP _gateway.domain > setevoy-work.45441: 63641 6/4/4 A 142.250.109.101, A 142.250.109.100, A 142.250.109.139, A 142.250.109.138, A 142.250.109.102, A 142.250.109.113 (260)
...

Аналогічно видна проблема з strace:

$ strace -r -e trace=network dig google.com
...
     0.002788 socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 15
...
     ;; communications error to 192.168.0.1#53: timed out
     5.005754 socket(AF_INET, SOCK_DGRAM, IPPROTO_IP) = 16
...

Тут в 0.002788 відкритий сокет для відправки запиту, а через 5 секунд (5.005754) – бо зараз dig запускався без +time=2 – відкривається новий сокет для нового запиту, бо на попередній відповіді не було.

В пошуках Немо проблеми

Тут опишу що взагалі перевіряв – квест вийшов той ще.

Хоча записав не все, робив більше, але основне зберіг – вже давно є звичка закидувати в чорнетку поста на RTFM під час дебагу проблем.

Перевірка DNS в Linux

Перше – що з DNS в системі?

В /etc/resolv.conf заданий роутер:

# Generated by NetworkManager
nameserver 192.168.0.1

Міняємо на 1.1.1.1 чи на 8.8.8.8 – проблема залишається.

Окей… Може, в системі ще якийсь активний резолвер, і починається “DNS-гонка в ядрі” – запит “блукає” між ними?

Перевіряємо systemd-resolved – ні, не запущений:

$ systemctl status systemd-resolved
○ systemd-resolved.service - Network Name Resolution
     Loaded: loaded (/usr/lib/systemd/system/systemd-resolved.service; disabled; preset: enabled)
     Active: inactive (dead)
...

Може, dnsmasq?

Теж виключений:

$ systemctl status dnsmasq
○ dnsmasq.service - dnsmasq - A lightweight DHCP and caching DNS server
     Loaded: loaded (/usr/lib/systemd/system/dnsmasq.service; disabled; preset: disabled)
     Active: inactive (dead)
...

Значить, DNS-запити йдуть напряму до роутера, і… Що? Тупить роутер з відповідями? До нього не доходять запити – іноді губляться?

Що це може бути?

  • локальний firewall на Linux чи роутері?
    • ні – вимикав, проблема залишалась
  • race між кількома локальними DNS-сервісами?
    • виключили вище
  • power management мережевої карти – вона уходить в sleep?
    • маловірогідно, але далі перевіряв і це
  • баг драйвера мережевої карти?
    • можливо, бо проблема з’явилась не так давно, до цього на цьому ноуті і цій системі все працювало без проблем
  • якісь проблеми конкретно з UDP?
    • теж ніж – робив dig +tcp google.com, проблема залишалась
  • відповідь на DNS-запит повертається з іншого IP?
    • екзотична ідея, але як варіант – на роутері кілька мережевих інтерфейсів, об’єднаних в bridge, і – теоретично – роутер може відправити відповідь з іншої
    • але це прям щось дуже неординарне, та і проблема виникала однаково на різних роутерах, і раніше її не було

IPv6 та DNS

Не пам’ятаю чому, але десь на початку грішив на IPv6 під час виконання DNS.

/etc/gai.conf керує алгоритмом вибору адрес у glibc (GAI = getaddrinfo()), і визначає яку адресу (IPv4 чи IPv6) програма, яка робила DNS-запит вибере першою у випадку, якщо DNS повернув і A, і AAAA записи.

Можна включити IPv4 first – розкоментувати строку:

...
precedence ::ffff:0:0/96 100
...

Перевіряємо, що повернеться першим – адреса IPv4, чи IPv6:

$ getent ahosts google.com
142.250.130.100 STREAM google.com
...  
2a00:1450:4025:800::64 STREAM 
...

Першим IPv4, але теж не допомогло.

Пробував виключити в ядрі IPv6 взагалі:

$ sudo sysctl -w net.ipv6.conf.all.disable_ipv6=1
$ sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1

Тут здавалось, що проблема знайдена – бо перший раз все пройшло без проблем, але ні – потім знов таймаути.

NIC Offloading

NIC Offloading – це коли частина операцій виконується на самому мережевому інтерфейсі, тобто offload деяких задач з CPU ноутбука на контролер карти.

Перевіряємо активні з ethtool -k:

$ sudo ethtool -k enp0s31f6 | grep on
rx-checksumming: on
tx-checksumming: on
        tx-checksum-ip-generic: on
scatter-gather: on
        tx-scatter-gather: on
tcp-segmentation-offload: on
...
generic-segmentation-offload: on
generic-receive-offload: on
rx-vlan-offload: on
tx-vlan-offload: on
receive-hashing: on
...

Самі цікаві тут:

  • TSO (TCP Segmentation Offloading): процесор віддає карті один великий шматок даних (наприклад, 64 КБ), а карта сама “нарізає” його на маленькі TCP-пакети по 1500 байт
  • GSO (Generic Segmentation Offloading): те саме, що й TSO, але більш універсальне (працює не лише для TCP)
  • GRO (Generic Receive Offloading): зворотний процес – карта отримує багато дрібних пакетів, “склеює” їх в один великий і лише тоді віддає процесору, що економить ресурси CPU
  • RX та TX Checksum Offloading: карта сама перевіряє контрольні суми (CRC) вхідних пакетів – якщо пакет “битий”, карта його просто викидає, навіть не повідомляючи операційну систему

По черзі вимикаємо їх, і перевіряємо:

  • sudo ethtool -K enp0s31f6 gro off: не допомогло
  • sudo ethtool -K enp0s31f6 gso off: не допомогло
  • sudo ethtool -K enp0s31f6 tso off: не допомогло
  • sudo ethtool -K enp0s31f6 rx off: не допомогло, і стало навіть гірше

Насправді те, що після відключення RX Checksum Offloading стало гірше – вже було підказкою: якщо до цього мережева карта сама фільтрувала помилки, то тепер вони всі повалили до ядра, що створило додаткове навантаження і хаос у черзі пакетів, тому корисні DNS-відповіді стали губитися ще частіше.

NIC Power Management

EEE (Energy Efficient Ethernet) має зменшувати витрати енергії на роботу карти.

Перевіряємо:

$ sudo ethtool --show-eee enp0s31f6
EEE settings for enp0s31f6:
enabled - active
17 (us)
        Supported EEE link modes:  100baseT/Full
                                   1000baseT/Full
        Advertised EEE link modes:  100baseT/Full
                                    1000baseT/Full
        Link partner advertised EEE link modes:  100baseT/Full
                                                 1000baseT/Full

Зараз “enabled – active” – вимикаємо:

$ sudo ethtool --set-eee enp0s31f6 eee off

Не допомогло.

Ще пробував так: запускаємо ping з короткими інтервалами, аби карта не засинала:

$ ping -i 0.2 192.168.0.1

І одночасно запускаємо цикл з dig – але проблема залишається.

Окремо перевіряв налаштування Runtime Power Management:

Знаходимо адресу PCI для девайсу enp0s31f6:

$ ls -l /sys/class/net/enp0s31f6/device
lrwxrwxrwx 1 root root 0 Jan 19 09:38 /sys/class/net/enp0s31f6/device -> ../../../0000:00:1f.6

Або:

an 19 09:38 /sys/class/net/enp0s31f6/device -> ../../../0000:00:1f.6
[setevoy@setevoy-work ~]  $ lspci -D | grep Ethernet
0000:00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (18) I219-LM (rev 20)

І перевіряємо параметри power:

$ cat /sys/bus/pci/devices/0000:00:1f.6/power/control
on

on” – включена постійно, значить не має вимикатись.

Драйвер та Message Signaled Interrupts

Перевіряв драйвер:

$ lspci -k -s 00:1f.6
00:1f.6 Ethernet controller: Intel Corporation Ethernet Connection (18) I219-LM (rev 20)
        Subsystem: Lenovo Device 2327
        Kernel driver in use: e1000e
        Kernel modules: e1000e

Мережевий контролер – Intel I219-LM, і драйвер e1000e, про який пишуть, що він “капризний”.

Параметри interrupts:

$ cat /proc/interrupts | grep -i enp0s31f6 ... IR-PCI-MSI-0000:00:1f.6 0-edge enp0s31f6

IR-PCI-MSI-0000:00:1f.6 – драйвер використовує MSI (Message Signaled Interrupts), яка начебто на Linux може давати drops для UDP на деяких картах Intel.

Створив файл /etc/modprobe.d/e1000e.conf, задав interrupt mode в legacy (див. Linux* Driver for Intel(R) Ethernet Network Connection):

options e1000e IntMode=0

Ребутнувся, перевірив:

$ cat /proc/interrupts | grep -i enp0s31f6
  19:     240716         ...  IR-IO-APIC   19-fasteoi   enp0s31f6

Не допомогло – проблема все ще залишалась.

Та і dig +tcp google.com все одно працював з проблемами.

Final: rx_crc_errors та зменшення швидкості

Ну і те, що спочатку пропустив – перевірка помилок на інтерфейсі.

Пропустив, бо кількість помилок не росла під час тестів:

$ ip -s link show enp0s31f6
3: enp0s31f6: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether c4:c6:e6:e7:e4:26 brd ff:ff:ff:ff:ff:ff
    RX:  bytes packets errors dropped  missed   mcast           
     750558152  589207    104       0       0       0 
    TX:  bytes packets errors dropped carrier collsns           
      40067575  157761      0       2       0       0 
    altname enxc4c6e6e7e426

Або з ethtool:

$ sudo ethtool -S enp0s31f6 | grep -E "errors|missed|dropped|timeout|tx_aborted" | grep -v ": 0"
     rx_errors: 114
     tx_dropped: 26
     rx_crc_errors: 57

rx_crc_errors каже, що проблема з цілісністю пакетів, і – якщо з роутерам і кабелем все в порядку (а проблема спостерігалась на різних роутерах і з різними кабелями) – то скоріш за все проблема в самому RJ-45 на ноутбуці, хоча контакти виглядають нормально.

Спробував примусово зменшити швидкість на інтерфейсі з гігабіта до 100 Mbps:

$ sudo ethtool -s enp0s31f6 speed 100 duplex full autoneg on

І чудо! Все працює!

Повертаємо знов 1000:

$ sudo ethtool -s enp0s31f6 speed 1000 duplex full autoneg on

І проблема знову з’являється.

Можна було б просто залишити 100 Mbps – але ж я не для того підключений по кабелю і плачу за гігабітний GPON?

Благо, вдома є кілька USB-адаптерів з Ethernet, перемкнув кабель на нього:

$ ip a s enp0s13f0u2u3
2: enp0s13f0u2u3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether c8:4d:44:29:27:6b brd ff:ff:ff:ff:ff:ff
    altname enxc84d4429276b
    inet 192.168.0.198/24 brd 192.168.0.255 scope global dynamic noprefixroute enp0s13f0u2u3
...

Є гігабіт і Full Duplex:

$ sudo ethtool enp0s13f0u2u3
Settings for enp0s13f0u2u3:
        Supported ports: [ TP    MII ]
        Supported link modes:   10baseT/Half 10baseT/Full
                                100baseT/Half 100baseT/Full
                                1000baseT/Half 1000baseT/Full
        ...
        Speed: 1000Mb/s
        Duplex: Full
        ...
                               drv probe link timer ifdown ifup rx_err tx_err tx_queued intr tx_done rx_status pktdata hw wol
        Link detected: yes

І тепер все працює без проблем.

Loading

FreeBSD: Home NAS, part 8 – backup даних NFS та Samba з restic
5 (1)

5 Січня 2026

Власне, по налаштуванню NAS вже зроблено майже все – з VPN є доступи з різних мереж, є різні шари, трохи затюнили безпеку.

Залишилось дві основні речі: моніторинг і бекапи, бо мати ZFS mirror на двох дисках з регулярними ZFS snapshots це, звісно, класно, але все одно недостатньо, а тому хочеться додатково робити бекапи десь в клауд.

Особливо я відчув необхідність мати доступ до бекапів в клаудах на початку війни, коли не ясно було де я опинюсь через годину, і чи буде в мене можливість забрати із собою бодай якесь залізо.

Сьогодні подумаємо і заплануємо як робити бекапи з Linux-хостів на NFS share та як робити бекапи даних NFS і Samba на FreeBSD. При чому бекапи хочеться зробити у два незалежних сховища – AWS S3 та Google Disk: S3 буде основним, а Гугл – резервною копією (резервної копії).

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

Планування бекапів

Отже, що в мене є:

  • Linux-хости: домашній і робочий ноутбуки – всякі фото, відео, музика, робочі дані, документи, ключі SSH/GPG/etc, конфіги самої системи
  • FreeBSD-хост: тут живуть Samba та NFS shares і маємо системні дані, які треба зберігати

NFS-шари підключаються до Linux-хостів, які сюди будуть робити бекапи, а потім вже з FreeBSD ці бекапи копіюються до AWS S3 та Google Drive.

Якщо це відобразити схематично, то виходить така картина:

FreeBSD backup plan

Якщо з ноутбуками і Linxu все більш-менш просто – бекапимо важливі дані із /home/setevoy, то з FreeBSD краще продумати окремо, бо тут даних більше:

  • /nas/nfs/backups: бекапи Linux-хостів – треба зберігати копії в клауд
  • /nas/smb: тут два датасети – /nas/smb/media зі всякими музикою-фільмами, та /nas/smb/private з приватними даними, обидва теж зберігати в клауд
  • плюс системні бекапи самої FreeBSD

В системний бекап FreeBSD можна включити:

  • /etc та /usr/local/etc: завжди мати копію всіх конфігураційних файлів
  • /root: якщо там файли типу nas-private-pass.key, яким в мене шифрується і монтується один з датасетів
  • /var/db: pkg і jails metadata, sqlite/db файли деяких сервісів

Вибір утиліт для бекапу

Спочатку думав взяти Timeshift, але, як виявилось, він не вміє в remote storage і не може зберігати на NFS.

А тому треба вибирати щось під мої потреби:

  • мультиплатформа – FreeBSD, Linux, Windows
  • вміти в повне і інкрементальне копіювання
  • мати можливість через конфіг-файл задавати список директорій та файлів, які треба бекапити або пропускати
  • must have – CLI
  • опціонально – мати Web або звичайний GUI
  • вмісти працювати як з локальними файловими системами – так і з NFS або Samba, і на додачу – вміти працювати з клаудами
  • коректно працювати з правами доступу та ACL файлової системи, бо файлові системі різні
  • опціонально – вміти шифрувати бекапи

З тих варіантів, що передивився – найбільше сподобався restic, тож вирішив спробувати його.

Restic overview

Вся його документація – Restic Documentation та Manual.

Основні плюшки restic:

  • написаний на Golang, див GitHub restic
  • доволі простий синтаксис команд для CLI
  • шифрування та data compression з коробки
  • інкрементальні бекапи через власні снапшоти
  • з коробки вміє працювати з NFS та AWS S3, див. Storage Backends.
  • може працювати з іншими бекендами через rclone, а вже rclone має просто безліч варіантів бекендів (див. Supported providers)
  • є third-party Web UI – Backrest
  • є клієнти під всі системи – Linux, FreeBSD, macOS, Windows (див. Installation)

Встановлюємо на Arch Linux:

$ sudo pacman -S restic

Та на FreeBSD:

# pkg install restic

Приклади в цьому пості будуть з Linux, але принципової різниці нема – клієнт працює однаково на всіх платформах.

Restic repositories та snapshots

Документація – Preparing a new repository та Repository Format.

Дані в restic організовані у репозиторіях: кожен репозиторій – це окремий каталог, який містить конфігурацію репозиторію, індекси та зашифровані дані.

Під час створення бекапу restic формує власні логічні snapshots. Дані розбиваються на незалежні блоки (blobs), які і є базовими одиницями зберігання в репозиторії.

При наступних бекапах restic перевіряє, які саме блоки були змінені, і копіює тільки їх, а на блоки даних, які не змінились – створює посилання з нового снапшоту, таким чином оптимізуючи зайнятий дисковий простір.

Тобто тут процес  схожий із ZFS snapshots – тільки у ZFS посилання створюється на блоки самої файлової системи і оригінальні дані, а в restic – на власні блоки даних в каталозі репозиторію.

При цьому restic зберігає дані у власному форматі, а тому ми не залежимо від файлової системи – створюємо бекап з ext4, копіюємо на ZFS, зберігаємо в S3, і відновлюємо на упасі боже Windows з NTFS. Єдине, що нам буде треба – це клієнт restic.

Створюємо тестовий репозиторій:

$ restic init --repo test-repo
enter password for new repository: 
enter password again: 
created restic repository 50ef450308 at test-repo

Перевіряємо зміст:

$ ll test-repo/
total 24
-r--------   1 setevoy setevoy  155 Jan  1 16:47 config
drwx------ 258 setevoy setevoy 4096 Jan  1 16:47 data
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 index
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 keys
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 locks
drwx------   2 setevoy setevoy 4096 Jan  1 16:47 snapshots

Restic та шифрування даних

Документація – Keys, Encryption and MAC.

Кожен репозиторій має ключ, який використовується для доступу до зашифрованих даних:

$ restic key list --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
 ID        User     Host          Created
-----------------------------------------------------
*0ca1c659  setevoy  setevoy-work  2026-01-01 16:49:49
-----------------------------------------------------

Шифрування даних виконується з master key, який зберігається в репозиторії:

$ restic -r test-repo cat masterkey
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
{
  "mac": {
    "k": "v1PIB3bB1VW46oWWBtKQYA==",
    "r": "Tia4a7HGs7PmN1EzWoWh4g=="
  },
  "encrypt": "0c3l/00P3dTgdbAqPZApAYn7E/MiloqOXyQsYr6AGOA="
}

Для отримання доступу до якого використовуються дані user keys:

$ cat test-repo/keys/0ca***f3a | jq
{
  "created": "2026-01-01T16:49:49.010471345+02:00",
  "username": "setevoy",
  "hostname": "setevoy-work",
  "kdf": "scrypt",
  "N": 32768,
  "r": 8,
  "p": 9,
  "salt": "F3G***50Q==",
  "data": "rjw***FLQ=="
}

Тут:

  • scrypt: KDF (Key Derivation Function), яка використовується для отримання криптографічного ключа з пароля (див. Scrypt Key Derivation Function)
  • salt: сіль з рандомним значенням
  • data: зашифрований master key – саме він використовується для шифрування даних

Коли restic потрібно отримати доступ до даних у репозиторії – він бере введений пароль і salt, передає їх у KDF і формує ключ, який використовується для розшифрування master key репозиторію.

Master key, у свою чергу, застосовується для шифрування та розшифрування ключів, якими вже безпосередньо шифруються дані та метадані в репозиторії.

При цьому можна мати кілька різних user keys (або access keys), які будуть використовуватись для отримання master key.

При потребі пароль можна змінити:

$ restic key passwd --repo test-repo/
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
enter new password: 
enter password again: 
saved new key as <Key of setevoy@setevoy-work, created on 2026-01-01 16:49:49.010471345 +0200 EET m=+13.105722833>

При налаштуванні автоматизації бекапів – пароль можна передати зі змінної оточення RESTIC_PASSWORD (див. Environment Variables) або з файлу через --password-file.

Наприклад, для використання паролю з файлу – створимо директорію:

$ mkdir -p ~/.config/restic-test
$ chmod 700 ~/.config/restic-test/

Генеруємо пароль:

$ pwgen 32 1
xoo8eibia2ohch7Oat7zeeshahn0keic

І зберігаємо його в файл ~/.config/restic-test/test-repo-password.

Задаємо доступ на читання тільки власнику:

$ chmod 600 ~/.config/restic-test/test-repo-password

Додаємо новий ключ для репозиторію:

$ restic key add --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
enter new password: 
enter password again: 
saved new key with ID c08c993b87363c17526e98fd46aeaf14767fa051e3b0d87a32c0cecc50e361d4

Перевіряємо ключі тепер:

[setevoy@setevoy-work ~/Projects/Restic]  $ restic key list --repo test-repo
enter password for repository: 
repository 50ef4503 opened (version 2, compression level auto)
 ID        User     Host          Created
-----------------------------------------------------
*0ca1c659  setevoy  setevoy-work  2026-01-01 16:49:49
 c08c993b  setevoy  setevoy-work  2026-01-01 17:02:10
-----------------------------------------------------

В *0ca1c659 зірочка показує, що зараз репозиторій відкритий з цим ключем.

Пробуємо відкрити з новим ключем – паролем з файла:

$ restic stats --repo test-repo --password-file ~/.config/restic-test/test-repo-password
repository 50ef4503 opened (version 2, compression level auto)
[0:00]          0 index files loaded
scanning...
Stats in restore-size mode:
     Snapshots processed:  0
              Total Size:  0 B

Restic backup та restore

Документація – Backing up.

Для створення бекапів використовуємо команду restic backup, а для відновлення, власне, restic restore.

Бекапимо файл /tmp/restic-test.txt в наш репозиторій:

$ restic backup /tmp/restic-test.txt --repo test-repo
repository 50ef4503 opened (version 2, compression level auto)
no parent snapshot found, will read all files
[0:00]          0 index files loaded

Files:           1 new,     0 changed,     0 unmodified
Dirs:            1 new,     0 changed,     0 unmodified
Added to the repository: 755 B (687 B stored)

processed 1 files, 13 B in 0:01
snapshot bf8def5f saved

Під час кожного виклику restic backup в репозиторії створюється новий snapshot, навіть якщо дані в source не змінювались.

Але, як писав вище – якщо не змінюються дані, то і розмір репозиторію не росте, бо restic просто створить посилання з нового снапшоту на старі блоки даних.

При зміні частини даних – відповідно будуть створені нові блоки тільки для нових даних, на які буде замаплений цей снапшот, а на незмінні дані – в новому снапшоті залишаться старі посилання.

Перевіряємо наявні снапшоти:

$ restic snapshots --repo test-repo
repository 50ef4503 opened (version 2, compression level auto)
ID        Time                 Host          Tags        Paths                 Size
-----------------------------------------------------------------------------------
bf8def5f  2026-01-01 17:07:53  setevoy-work              /tmp/restic-test.txt  13 B
-----------------------------------------------------------------------------------
1 snapshots

Основні корисні команди при роботі з репозиторіями та снапшотами:

  • restic stats: статистика по репозиторію або снапшоту
  • restic check: перевірка цілісності репозиторію
  • restic ls: подивитись зміст снапшоту
  • restic diff: порівняти дані у двох снапшотах
  • restic copy: скопіювати зміст одного репозиторію в інший

І окремо варто згадати --dry-run – перевірити що саме буде виконуватись, і яких даних торкнеться операція.

Для відновлення даних з бекапу використовуємо restic restore і вказуємо ID снапшоту та куди його відновити.

Якщо в destination каталогу нема – restic його створить, а в ньому відновить ієрархію каталогів та файлів зі снапшоту:

$ restic restore --repo test-repo bf8def5f --target /tmp/test-restic-restore
...
restoring snapshot bf8def5f of [/tmp/restic-test.txt] at 2026-01-01 17:07:53.016664301 +0200 EET by setevoy@setevoy-work to /tmp/test-restic-restore
Summary: Restored 2 files/dirs (13 B) in 0:00

Перевіряємо:

$ tree /tmp/test-restic-restore
/tmp/test-restic-restore
└── tmp
    └── restic-test.txt

Include та exclude для backup та restore

При створенні бекапу з restic backup ми вказуємо шлях, який бекапиться, а тому окремої опції --include нема.

Але є --exclude, з яким можна вказати які дані не треба включати в снапшот.

Наприклад, маємо каталог:

$ tree /tmp/restic-dir-test
/tmp/restic-dir-test
├── a.txt
└── sub
    └── b.txt

Бекапимо весь цей каталог, але пропускаємо файл a.txt:

$ restic backup /tmp/restic-dir-test --exclude /tmp/restic-dir-test/a.txt --repo test-repo

Дивимось в снапшоті – a.txt нема:

$ restic ls dfd8271d --repo test-repo
...
/tmp
/tmp/restic-dir-test
/tmp/restic-dir-test/sub
/tmp/restic-dir-test/sub/b.txt

Для restic restore можемо вказати як --include – що саме відновити, так і --exclude – які дані зі снапшота пропустити.

Замість передачі include/exclude в CLI можна всі шляхи описати в файлах, по одному на строку, див. Including Files та Excluding Files.

У файлах можна використовувати коментарі та пусті строки, наприклад, файл backup-nfs.list:

# MAIN
/home/setevoy/Photos
...

# dotdirs
/home/setevoy/.aws
...

Потім викликаємо як:

  • restic backup --files-from backup-nfs.list -r <REPO_NAME>
  • restic backup --files-from backup-nfs.list --exclude-file exclude-nfs.list -r <REPO_NAME>

І аналогічно з restic restore та --include-file і --exclude-file.

Крім того, в include та exclude можна використовувати globbing (не regex):

  • *: – будь-яка послідовність символів в межах одного рівня каталогу
    • приклад: *.log, cache/*
  • **: будь-яка кількість каталогів рекурсивно
    • приклад: **/data, /home/**/cache
  • ?: один будь-який символ
    • приклад: file?.txt
  • [abc]: один символ з набору
    • приклад: file[12].txt
  • [a-z]: діапазон символів
    • приклад: img[0-9].jpg
  • !pattern: інверсія правила (тільки у файлах include/exclude)

Теги для снапшотів

При створенні снапшотів в ZFS ми можемо вказати його ім’я через @.

В restic снапшоти зберігаються тільки з ID – але до них можна додати теги:

$ restic backup /tmp/restic-dir-test --repo test-repo --tag "daily" --tag "$(date +"%Y-%m-%d-%H-%M")"

Далі по цим тегам можна виконувати пошук, copy, restore, forget (про forget далі).

Наприклад, вивести тільки снапшоти з тегом daily:

$ restic snapshots --tag daily -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
d14ecde9  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Видалення снапшотів з forget та prune

Документація – Removing backup snapshots.

Для очистки даних restic використовуємо команди forget та prune:

  • restic forget: знімає посилання снапшота на блоки даних – але не видаляє їх
  • restic prune: видаляє самі дані – блоки, на які нема посилань зі снапшотів

Наприклад:

$ restic forget f3ce1ac3 -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
[0:00] 100.00%  1 / 1 files deleted

Або відразу виконати prune через restic forget --prune:

$ restic forget 45e6f909 -r test-repo --prune
repository 50ef4503 opened (version 2, compression level auto)
[0:00] 100.00%  1 / 1 files deleted
1 snapshots have been removed, running prune
loading indexes...
[0:00] 100.00%  7 / 7 index files loaded
loading all snapshots...
finding data that is still in use for 5 snapshots
[0:00] 100.00%  5 / 5 snapshots
...
repacking packs
[0:00] 100.00%  1 / 1 packs repacked
rebuilding index
[0:00] 100.00%  8 / 8 indexes processed
[0:00] 100.00%  8 / 8 old indexes deleted
removing 2 old packs
[0:00] 100.00%  2 / 2 files deleted
done

Замість snapshot ID можна вказати policy, див. Removing snapshots according to a policy.

Наприклад:

$ restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --tag daily --prune -r test-repo
repository 50ef4503 opened (version 2, compression level auto)
Applying Policy: keep 7 daily, 4 weekly, 6 monthly snapshots
keep 1 snapshots:
ID        Time                 Host          Tags                    Reasons           Paths                 Size
-----------------------------------------------------------------------------------------------------------------
d14ecde9  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  daily snapshot    /tmp/restic-dir-test  18 B
                                                                     weekly snapshot
                                                                     monthly snapshot
-----------------------------------------------------------------------------------------------------------------

Тут:

  • --keep-daily 7: залишаємо снапшоти за останні 7 днів
  • --keep-weekly 4: залишаємо снапшоти за останні 4 тижні (по одному snapshot на тиждень)
  • --keep-monthly 6: залишаємо снапшоти за останні 6 місяців (по одному snapshot на місяць)
  • застосовуємо тільки для снапшотів з тегом daily, і відразу видаляємо дані з диску

Restic mount – репозиторій як директорія

Можна змонтувати репозиторій як звичайну папку, монтується тільки в режимі read-only:

$ mkdir /tmp/restic-mounted-test-repo

$ restic mount -r test-repo /tmp/restic-mounted-test-repo
...
Now serving the repository at /tmp/restic-mounted-test-repo
...

І отримуємо доступ до даних в усіх снапшотах:

$ tree /tmp/restic-mounted-test-repo
/tmp/restic-mounted-test-repo
├── hosts
│   └── setevoy-work
│       ├── 2026-01-01T17:07:53+02:00
│       │   └── tmp
│       │       └── restic-test.txt
...
├── ids
│   ├── 0f477146
│   │   └── tmp
│   │       └── restic-dir-test
│   │           └── sub
│   │               └── b.txt
...
├── snapshots
│   ├── 2026-01-01T17:07:53+02:00
│   │   └── tmp
│   │       └── restic-test.txt
...

Але це можливість більше для якогось ручного дебагу-фіксу, а не для автоматизації.

Restic copy – копіювання даних між репозиторіями

Для копіювання одного репозиторію в інший використовуємо restic copy, див. Copying snapshots between repositories.

Наприклад, так можна копіювати бекапи з репозиторіїв у NFS на FreeBSD до репозиторіїв в AWS S3 та Google Drive.

По-дефолту restic copy скопіює всі снапшоти із source repo, але можна вказати які саме снапшоти копіювати.

Створюємо новий пустий репозиторій:

$ restic init -r new-test-repo

Копіюємо один снапшот зі старого репозиторію:

$ restic copy --from-repo test-repo --repo new-test-repo d14ecde9

Або всі снапшоти з тегом daily:

$ restic copy --from-repo test-repo --repo new-test-repo --tag daily

Тепер в новому репозиторії маємо ті самі дані:

 $ restic snapshots -r new-test-repo
repository fc8a407c opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
7335e7bf  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Restic та репозиторій в AWS S3

З S3 все більш-менш аналогічно до роботи з локальними репозиторіями, але є деякі нюанси.

Для аутентифікації restic використовує звичайний механізм – пошук змінних оточення AWS_ACCESS_KEY_ID та AWS_SECRET_ACCESS_KEY, або пошук у файлах ~/.aws/config та ~/.aws/credentials.

Задаємо змінні:

$ export AWS_PROFILE=setevoy
$ export AWS_DEFAULT_REGION=eu-west-1

В AWS створюємо корзину, а потім ініціалізуємо в ній репозиторій, використовуючи формат s3:s3.amazonaws.com/<BUCKET_NAME>/<REPO_NAME>:

$ restic init --repo s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository
created restic repository 58303a9c88 at s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository

Перевіряємо корзину:

$ aws s3 ls s3://test-restic-repo-bucket --recursive
2026-01-04 16:11:28        155 test-restic-repository/config
2026-01-04 16:11:28        457 test-restic-repository/keys/e53...22f

Важливі нюанси, які треба мати на увазі при роботі з S3:

  • видаляти дані з репозиторію restic в AWS S3 можна тільки через restic forget та restic prune
  • використання S3 Lifecycle rules для restic не рекомендується – навіть для зміни storage class

Каталоги (index/, snapshots/, keys/) активно використовуються restic; якщо перенести, наприклад, keys/ у Glacier або Deep Archive – restic може зависати або падати по таймауту, очікуючи доступ до ключів.

Теоретично lifecycle transitions можна застосувати лише до каталогу data/, де зберігаються pack-файли з даними, але якщо потім запустити restic prune – то restic буде потрібен доступ до старих pack-файлів в data/, і, якщо вони знаходяться в Glacier або Deep Archive, операція стане або дуже повільною, або взагалі неможливою

Тому краще просто робити періодичний restic forget і restic prune, та залишити S3 Standart class даних в бакеті.

Restic та Google Drive через rclone

В мене rclone для Google Drive вже налаштований, допишу про нього окремо, вже є в чернетках, бо теж дуже цікава система.

Що ми можемо зробити – це використати rclone як storage backend для роботи з типами storage, яких нема в самому restic.

Але працює ця схема ну дуже повільно (принаймні з Google Drive) – тому її краще використовувати як one time copy, а не для регулярних бекапів.

Документація – rclone serve restic.

Наприклад, є rclone profile:

$ rclone config show
[setevoy-google-drive]
type = drive
...

Через який я можу підключатись до Google Disk:

$ rclone lsd setevoy-google-drive:
           0 2025-04-16 16:32:56        -1 Arch_Old_Music
           0 2025-04-16 16:50:53        -1 Arch_Work_Photos
           0 2020-06-19 22:13:53        -1 BackendParty-2020-06
...

Створюємо там новий каталог restic-rclone-gdrive-repo:

$ rclone mkdir setevoy-google-drive:restic-rclone-gdrive-repo

З rclone serve restic запускаємо локальний HTTP enpoint нового бекенду для restic вказуючи створений вище каталог – це вже буде корінь репозиторію:

$ rclone serve restic setevoy-google-drive:restic-rclone-gdrive-repo
2026/01/04 16:39:34 NOTICE: Google drive root 'restic-rclone-gdrive-repo': Serving restic REST API on [http://127.0.0.1:8080/]

В іншому вікні для restic задаємо змінну нового ендпоінта:

$ export RESTIC_REPOSITORY=rest:http://127.0.0.1:8080/

Виконуємо ініціалізацію цього репозиторію:

$ restic init
enter password for new repository: 
enter password again: 
created restic repository e1a8edaebd at rest:http://127.0.0.1:8080/

Перевіряємо дані в Google Drive:

$ rclone lsd setevoy-google-drive:restic-rclone-gdrive-repo
           0 2026-01-04 16:42:08        -1 data
           0 2026-01-04 16:42:09        -1 index
           0 2026-01-04 16:42:10        -1 keys
           0 2026-01-04 16:42:11        -1 locks
           0 2026-01-04 16:42:12        -1 snapshots

І скопіюємо дані з AWS S3 до репозиторію в Google Drive

Задаємо змінні:

$ export AWS_PROFILE=setevoy
$ export AWS_DEFAULT_REGION=eu-west-1
$ export RESTIC_REPOSITORY=rest:http://127.0.0.1:8080/

Запускаємо restic copy, але тепер для copy вказуємо тільки --from-repo – бо destination у нас вже заданий через $RESTIC_REPOSITORY:

$ restic copy --from-repo s3:s3.amazonaws.com/test-restic-repo-bucket/test-restic-repository
enter password for source repository: 
repository 58303a9c opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
enter password for repository: 
repository e1a8edae opened (version 2, compression level auto)
created new cache in /home/setevoy/.cache/restic
[0:00]          0 index files loaded
[0:00]          0 index files loaded

Перевіряємо в Google Drive:

$ restic snapshots 
enter password for repository: 
repository e1a8edae opened (version 2, compression level auto)
ID        Time                 Host          Tags                    Paths                 Size
-----------------------------------------------------------------------------------------------
...
bb02e44b  2026-01-04 15:08:41  setevoy-work  daily,2026-01-04-15-08  /tmp/restic-dir-test  18 B
-----------------------------------------------------------------------------------------------

Що треба мати на увазі при роботі restic через clone:

  • не використовувати rclone mount
  • не виконувати запис одночасно з двох restic клієнтів
  • не використовувати одночасно два rclone serve restic для одного репозиторію

Власне, на цьому все.

Залишилось додати автоматизацію запуску бекапів на Linux та FreeBSD, але це вже опишу окремим постом.

На додачу – документація по тюнингу restic: Tuning Backup Parameters.

Loading

TCP/IP: SYN flood атака на сервер RTFM, та “Hacker News hug of death”
5 (2)

2 Січня 2026

Прилітає сьогодні зранку алерт від моніторингу, що блог впав:

Ну, думаю – знов якийсь DDoS, не перший раз.

Investigating the issue

Йду в Cloudflare, вмикаю Under Attack Mode, і починаю розбиратись.

Дивлюсь запити:

Ага, думаю, ващє фігня – з одного IP запити, зараз його забаню, і готово.

Додаю нове правило з Action = Block в Cloudflare Security Rules, і пішов глянути – що з IP такий?

Whois каже, що якийсь хост з DigitalOcean:

$ whois 46.101.201.123
...
inetnum:        46.101.128.0 - 46.101.255.255
abuse-c:        AD10778-RIPE
netname:        DIGITALOCEAN
...

Там часто всякі боти запускаються, нічого незвичного.

Далі, вирішив з nmap глянути що за сервіси є на тому атакуючому хості:

# nmap -sS 46.101.201.123
...
PORT     STATE    SERVICE
22/tcp   open     ssh
80/tcp   open     http
443/tcp  open     https
...

Хм, думаю – дивно, що за бот такий, що має 80 і 443 порти?

Відкриваю https://46.101.201.123 в браузері, і… попадаю на власний блог 🙂

Шта?…

Перевіряю, які IP в мене в DigitalOcean, і:

Тобто – да, 46.101.201.123 – це Droplet IP сервера, на якому хоститься RTFM.

Хоча взагалі на DNS в IN A для rtfm.co.ua використовується DigitalOcean Reserved IP, який можна переключати між дроплетами:

Тобто:

  • NS для rtfm.co.ua – Cloudflare
  • на них IN A 67.207.75.157
  • Droplet IP 46.101.201.123 не вказаний ніде
  • але запити йдуть на нього

Окей…

Тут ще буде окреме питання – чому в CloudFlare показувались запити від 46.101.201.123 – але про це в кінці.

SYN flood та підключення в SYN_RECV 

Пішов глянути що взагалі на сервері в нетворкінгу, які активні конекти?

А там…

Купа підключень в статусі SYN_RECV – класичний SYN flood: клієнт нам відправляє TCP-пакет з флагом SYN, ми йому відповіли з SYN-ACK, і чекаємо на ACK від нього – але він не приходить, а ресурси CPU/RAM сервера на очікування зайняті (див. TCP handshake, нещодавно писав).

Mitigating the issue

Так як підключення йдуть не через CloudFalre – то і його Security Rules нам не допоможуть.

А Network Firewall в Digital Ocean, як і Security Groups в AWS вміють тільки в Allow правила – але не в Deny (в AWS можна зробити Deny через правила у VPC NACL – Network Access Control List).

Linux Kernel TCP tuning

В першу черги тюнимо параметри TCP-стеку ядра:

# sysctl -w net.ipv4.tcp_syncookies=1
net.ipv4.tcp_syncookies = 1
# sysctl -w net.ipv4.tcp_max_syn_backlog=4096
net.ipv4.tcp_max_syn_backlog = 4096
# sysctl -w net.ipv4.tcp_synack_retries=2
net.ipv4.tcp_synack_retries = 2

Тут:

  • net.ipv4.tcp_syncookies: вмикаємо SYN cookies – ядро може не тримати стан TCP-підключення, коли backlog переповнений
  • net.ipv4.tcp_max_syn_backlog: збільшуємо розмір беклогу для SYN/SYN-ACK, аби реальні клієнти не відвалювались
    • дефолт 256
  • net.ipv4.tcp_synack_retries: обмежуємо кількість спроб ядра відповісти на SYN – скільки раз шлемо SYN-ACK, якщо клієнт не повернув ACK
    • дефолт 5

Вже стало краще:

 

Далі можна на DigitalOcean firewall дозволити доступ тільки з мереж Cloudflare – але вони змінюються, а робити якусь авторизацію зараз влом.

Iptables та DROP by Source Address

Можна, звісно, банити атакуючі мережі – на момент перевірки була одна 177.36.16.0/20:

# netstat -anp | grep 46.101.201.123 | grep SYN_RECV
tcp        0      0 46.101.201.123:443      177.36.16.214:15795     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.16.152:43548     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.17.0:43309       SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      177.36.17.237:47283     SYN_RECV    -

Додаємо правило з DROP і логуванням для перевірки:

# iptables -I INPUT -s 177.36.16.0/20 -j LOG --log-prefix "DROP 177.36.16.0/20 "

Дивимось, чи працює правило:

# journalctl -k | grep "DROP 177.36.16.0/20" | head
Jan 02 08:34:35 setevoy-do-2023-09-02 kernel: DROP 177.36.16.0/20 IN=eth0 OUT= MAC=de:71:8d:d9:82:55:fe:00:00:00:01:01:08:00 SRC=177.36.16.242 DST=46.101.201.123 LEN=52 TOS=0x00 PREC=0x00 TTL=54 ID=40235 DF PROTO=TCP SPT=17587 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 
Jan 02 08:34:37 setevoy-do-2023-09-02 kernel: DROP 177.36.16.0/20 IN=eth0 OUT= MAC=de:71:8d:d9:82:55:fe:00:00:00:01:01:08:00 SRC=177.36.16.193 DST=46.101.201.123 LEN=52 TOS=0x00 PREC=0x00 TTL=56 ID=8752 DF PROTO=TCP SPT=45940 DPT=443 WINDOW=65535 RES=0x00 SYN URGP=0 
...

Залишаємо правило, але прибираємо запис в лог, бо це зайве навантаження на диск і систему:

# iptables -R INPUT 1 -s 177.36.16.0/20 -j DROP

Вже краще – але, очікувано, пішли підключення з інших адрес:

# netstat -anp | grep 46.101.201.123 | grep SYN_RECV
tcp        0      0 46.101.201.123:443      45.94.171.239:48242     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      146.103.26.224:30654    SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      91.124.63.174:45287     SYN_RECV    -                   
tcp        0      0 46.101.201.123:443      194.116.228.226:15311   SYN_RECV    -

Iptables та DROP by Destination Address

Ну і насправді тут є дуже просте рішення:

  • валідні запити мають йти тільки на Reserved IP, який вказаний в Cloudflare DNS
  • запити на Droplet IP на порт 443 взагалі не мають приходити

Тому просто банимо їх з iptables:

# iptables -A INPUT -p tcp -d 46.101.201.123 --dport 443 -j DROP

Тепер жодного SYN_RECV не залишилось.

Зберігання правил з iptables-persistent

Перевіряємо правила зараз:

# iptables -L INPUT -n --line-numbers
Chain INPUT (policy ACCEPT)
num  target     prot opt source               destination         
1    DROP       0    --  177.36.16.0/20       0.0.0.0/0           
2    DROP       6    --  0.0.0.0/0            46.101.201.123       tcp dpt:443

Аби зберігати їх при ребутах системи – встановлюємо iptables-persistent:

# apt install iptables-persistent

Під час установки він запропонує зберегти правила в файл /etc/iptables/rules.v4:

Перевіряємо що там:

# cat /etc/iptables/rules.v4 | grep 46.101.201.123
-A INPUT -d 46.101.201.123/32 -p tcp -m tcp --dport 443 -j DROP

Готово.

Але це буде працювати, поки не почнуть флудити на сам Reserved IP 67.207.75.157.

Тоді вже доведеться робити через дозвіл тільки з Cloudflare IPs.

Bonus: WordPress, Cloudflare та запити від Droplet IP

Але після того, як із SYN flood начебто розібрався – графіки кількості запитів в Cloudflare не зменшились.

І це логічно – бо SYN взагалі йшов напряму до сервера на IP 46.101.201.123, а не через Cloudflare, а там ці запити в Clodflare взагалі не трекались.

При цьому в логах Cloudflare від IP 46.101.201.123 всюди був один і той самий Path до файлу /wp-content/uploads/2025/11/freebsd_logo1.jpg:

Графіки Cloudflare за останні 6 годин виглядали так – в топі Source IPs зліва внизу саме 46.101.201.123:

Тут я вже напрягся:

  • SYN flood почався близько 10 ранку за Києвом
  • в цей жеж час є спайк запитів до Cloudflare від самого сервера RTFM

Тобто виглядало так, ніби на сервері якийсь код постійно звертається до одного і того самого URL на самому сервері.

Відключив Cloudflare WordPress плагін – ні, запити не спадають.

Відключив WP_CRON – теж не допомогло.

WTF is going on? – судорожно думав я, і додумався, що пора б включити і подивитись NGINX access logs і подивитись, що взагалі на сервер приходить.

А access logs побачив купу записів виду:

...
[02/Jan/2026:11:07:35 +0000] "GET /en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/ HTTP/1.1" 200 35451 "-" "HackerNews/1536 CFNetwork/3860.200.71 Darwin/25.1.0"
[02/Jan/2026:11:07:42 +0000] "GET /en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/ HTTP/1.1" 200 35462 "https://news.ycombinator.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"
[02/Jan/2026:11:07:43 +0000] "GET /wp-json/pvc/v1/increase/33806 HTTP/1.1" 200 99 "https://rtfm.co.ua/en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Mobile Safari/537.36"
...

HackerNews, ycombinator? Вау…

А друге – сама причина 46.101.201.123 в логах Clodflare: “GET /wp-json/pvc/v1/increase/” – це запит до WordPress-плагіна Page View Count, який не так давно включив. А “https://rtfm.co.ua/en/freebsd-home-nas-part-1-configuring-zfs-mirror-raid1″ – звідки запит був зроблений.

Тобто, плагін на сторінці поста робить запит, аби збільшити лічильник переглядів – при цьому підставляючи Referrer у вигляді тої сторінки, звідки він запит робить.

Cloudflare жеж бачить, що запит йде від Origin – і використовує в логах Droplet IP.

Ну а далі вже проста перевірка – що коїться взагалі з постом FreeBSD: Home NAS, part 1 – configuring ZFS mirror (RAID1):

Це при тому, що зазвичай переглядів кілька десятків, ну максимум 100-200.

І перевірка в гуглі вже показала причину такого напливу:

А трапилось те, що я сьогодні вранці перший раз запостив лінк на https://lobste.rs, звідки його перепостили на Hacker News, і я отримав “Hacker News hug of death” – див. Surviving the Hug of Death, де у людини була схожа ситуація.

Після відключення плагіну Page View Count – Droplet IP 46.101.201.123 в Cloudflare зник.

Loading

FreeBSD: Home NAS, part 7 – NFSv4 та підключення до Linux
5 (1)

31 Грудня 2025

Наступний крок в процесі налаштування домашнього NAS на FreeBSD – додати NFS share.

Samba зробили в попередній частині – тепер до неї додамо шари з NFS: моя ідея в тому, щоб Samba share використовувалась для всяких медіа-ресурсів, до яких потрібен доступ з телефонів та TV, а NFS буде виключно для Linux-хостів – двох ноутбуків в різних мережах (дім і офіс), які на цей розділ будуть робити свої бекапи з rsync, rclone або restic.

Всі частини серії по налаштуванню домашнього NAS на FreeBSD:

NFSv3 vs NFSv4

Наразі є дві основні версії NFS – v3 та v4.

FreeBSD підтримує робота з обома (я під час тестів налаштовував і v3 теж), але, очевидно, що v4 більш актуальна, і має низку переваг:

  • NFSv3 – stateless, а NFSv4 – stateful: v4 зберігає стан клієнтів і сесій, що спрощує роботу з блокуваннями та доступом до файлів
  • NFSv3 вважається простішою: але, як на мене, то NFSv4 налаштовується не складніше, і навіть простіше через меншу кількість компонентів
  • аутентифікація:
    • NFSv3 аутентифікація – IP клієнта + UID/GID (хто підключився і від імені якого користувача)
    • NFSv4 – розширена модель доступу, підтримка ACL (і база для Kerberos, якщо потрібно)

Див. NFSv3 and NFSv4: What’s the difference?

Створення ZFS datasets

Аби мати можливість окремих налаштувань снапшотів і ZFS quotes – датасети для NFS зробимо ієрархічно:

  • nas/: корневий датасет ZFS-пулу
    • nas/nfs/: корневий датасет для всього NFS
      • nas/nfs/backups/: датасет для бекапів з інших машин

А в nas/nfs/backups/ вже будуть окремі каталоги з іменами хостів для їхніх бекапів – “setevoy-home“, “setevoy-work“, “setevoy-rtfm” і т.д.

Пізніше, при потребі, в nas/nfs/ можна буде додати нову NFS-шару.

Додаємо новий dataset:

root@setevoy-nas:/home/setevoy # zfs create nas/nfs

В ньому створюємо другий, для бекапів:

root@setevoy-nas:/home/setevoy # zfs create nas/nfs/backups

Сам NFS daemon в системі є з коробки, нам треба тільки додати його запуск:

root@setevoy-nas:/home/setevoy # sysrc nfs_server_enable="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsv4_server_enable="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsv4_server_only="YES"
root@setevoy-nas:/home/setevoy # sysrc nfsuserd_enable="YES"

NFSv4 працює з іменами, а не “сирими” UID/GID, тому, аби ID коректно мапились в імена – додаємо в /etc/sysctl.conf, і активуємо зміни зараз:

root@setevoy-nas:/home/setevoy # sysctl vfs.nfs.enable_uidtostring=1
vfs.nfs.enable_uidtostring: 0 -> 1
root@setevoy-nas:/home/setevoy # sysctl vfs.nfsd.enable_stringtouid=1
vfs.nfsd.enable_stringtouid: 0 -> 1

NFS, ZFS та sharenfs 

ZFS підтримує налаштування NFS для датасетів через dataset property sharenfs (а для Samba – через sharesmb), хоча є має обмеження.

Для NFSv4 обов’язково треба вказати корневу директорію, в якій будуть самі шари, див. NFS Version 4 Protocol.

Додаємо її в файл /etc/exports:

V4: /nas/nfs

Але доступу до цього корневого каталогу не буде, поки ми не додамо його явно, наприклад як:

# zfs set sharenfs="-network 192.168.0.0/24 -ro" nas/nfs

Тепер можемо через sharenfs розшарити nas/nfs/backups датасет, для якого з -network вказуємо з яких адрес буде дозволений доступ:

root@setevoy-nas:/home/setevoy # zfs set sharenfs="-network 192.168.0.0/24" nas/nfs/backups

Перевіряємо:

root@setevoy-nas:/home/setevoy # zfs get sharenfs nas/nfs/backups
NAME             PROPERTY  VALUE                    SOURCE
nas/nfs/backups  sharenfs  -network 192.168.0.0/24  local

По факту, zfs set sharenfs просто редагує файл /etc/zfs/exports, який потім читається mountd:

root@setevoy-nas:~ # cat /etc/zfs/exports
# !!! DO NOT EDIT THIS FILE MANUALLY !!!

/nas/nfs/backups        -network 192.168.0.0/24

Запускаємо nfsd:

root@setevoy-nas:/home/setevoy # service nfsd start

nfsd автоматом запустить mountd, який власне відповідає за NFS sharing і читає конфігурацію з /etc/exports та /etc/zfs/exports:

root@setevoy-nas:~ # ps aux | grep mount
root      9475   0.0  0.0     13888   1076  -  Is   05:46       0:00.00 /usr/sbin/mountd -r -S -R /etc/exports /etc/zfs/exports

Додаємо правило на фаєрволі:

...
pass in on em0 proto { tcp udp } from { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 } to (em0) port 2049
...

Перевіряємо синтаксис, застосовуємо зміни pf:

root@setevoy-nas:~ # pfctl -nvf /etc/pf.conf && service pf reload

Монтуємо на клієнті – вказуємо явно -t nfs4:

[setevoy@setevoy-work ~] $ sudo mkdir /mnt/test/
[setevoy@setevoy-work ~] $ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test/

В 192.168.0.2:/backups каталог /backups вказуємо від корня, який задали в /etc/exports: тобто корінь у нас “/nas/nfs” – значить на клієнтах він буде “/“, і, відповідно, внутрішні датасети монтуємо від цього корня як /backups.

Перевіряємо маунти на клієнті:

[setevoy@setevoy-work ~]  $ findmnt /mnt/test/
TARGET SOURCE            FSTYPE OPTIONS
/mnt/test
       192.168.0.2:/backups
                         nfs4   rw,relatime,vers=4.2,rsize=131072,wsize=131072,namlen=255,hard,fatal_neterrors=none,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.4,local_lock=none,addr

З цікавого для нас тут:

  • nfs4: протокол
  • vers=4.2: версія
  • sec=sys: аутентифікація клієнтів (буде важливо для /etc/fstab на клієнтах, див. в кінці)
  • clientaddr=192.168.0.4: і адреса самого клієнта

Аналогічно на клієнті можна перевірити активне підключення з nfsstat -m:

[setevoy@setevoy-work ~]  $ nfsstat -m
/mnt/test from 192.168.0.2:/backups
 Flags: rw,relatime,vers=4.2,rsize=131072,wsize=131072,namlen=255,hard,fatal_neterrors=none,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.0.4,local_lock=none,addr=192.168.0.2

А на сервері – з nfsdumpstate:

root@setevoy-nas:/home/setevoy # nfsdumpstate
Flags         OpenOwner      Open LockOwner      Lock     Deleg  OldDeleg Clientaddr
                      0         0         0         0         0         0 192.168.0.4

А з опцією -l можна подивитись активні Opens and Locks – але зараз тут пусто.

Перевіряємо доступи – створимо файл на сервері:

root@setevoy-nas:/home/setevoy # touch /nas/nfs/backups/test-server

І дивимось на клієнті:

[setevoy@setevoy-work ~]  $ ll /mnt/test/
total 1
-rw-r--r-- 1 root root 0 Dec 31 13:12 test-server

(Dec 31 – коли б ще сетапити домашній NAS на FreeBSD, правда? 😀 )

NFS, users та “Permission denied”

Але з клієнта ми зараз не можемо нічого писати в каталог:

[setevoy@setevoy-work ~]  $ touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

Бо на сервері він створювався від root:

root@setevoy-nas:~ # stat /nas/nfs/backups/test-server
4446369902026857636 4 -rw-r--r-- 1 root wheel 0 0 "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" "Dec 31 13:12:36 2025" 131072 1 0x800 /nas/nfs/backups/test-server

Ба більше – доступу нема навіть від локального root на клієнті:

[setevoy@setevoy-work ~]  $ sudo touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

А по-дефолту NFS виконує root_squash, і всі операції від клієнтів на сервері виконує від локального юзера nobody.

Є кілька варіантів вирішення:

  • на сервері створити групу типу nfsusers, дати їй права на запис в каталог (775), і додати локального юзера setevoy в цю групу
    • самий кошерний і безпечний варіант
  • або можна задати опцію -maproot=root – тоді root на клієнті буде == root на сервері (UID в обох 0)
    • але це тільки про доступ до файлів, і тільки в межах NFS root – /nas/nfs
    • ОК варіант для домашнього NAS
  • трохи безпечніший варіант – вказати -maproot=setevoy і на сервері змінити власника /nas/nfs/backups/ – тоді операції від root на клієнті на сервері будуть виконуватись від UID/GID юзера setevoy на сервері
  • або взагалі зробити -mapall=root – і тоді всі юзери на клієнті будуть виконувати операції як локальний root
    • аналогічно до -maproot=root, але і самий небезпечний варіант

Так як ця шара для бекапів, які на клієнтах будуть виконуватись від root – то можна використати maproot=root:

root@setevoy-nas:/home/setevoy # zfs set sharenfs="-network 192.168.0.0/24 -maproot=root" nas/nfs/backups
root@setevoy-nas:/home/setevoy # service nfsd restart

Тепер на клієнті від звичайного юзера у нас доступу все ще нема:

[setevoy@setevoy-work ~]  $ touch /mnt/test/test-client
touch: cannot touch '/mnt/test/test-client': Permission denied

Але є від локального root, бо він отримує права root на сервері:

[setevoy@setevoy-work ~]  $ sudo touch /mnt/test/test-client

ZFS sharenfs та muliple -network

Дуже довго проковирявся з цим 🙁

Допомогли на форумі FreeBSD, див. NFSv4 and share for multiply networks (взагалі, FreeBSD community дуже відкрите, і набагато менш токсичне, аніж ті ж форуми Arch Linux).

В чому проблема: в мене доступ до хоста FreeBSD відбувається з кількох мереж (див. FreeBSD: Home NAS, part 3 – WireGuard VPN, Linux peer та routing):

  • 192.168.0.0/24: мережа офісу
  • 192.168.100.0/24:  домашня мережа
  • 10.8.0.0/24: VPN

І, звісно, хочеться доступ до NFS мати захищеним на рівні мереж (хоча в моєму випадку цілком можна було обійтись без цього, але краще відразу робити правильно).

А проблема полягає в том, що у ZFS ver 2.2.7, яка використовується у FreeBSD 14.3 зараз, нема можливості вказати кілька мереж в sharenfs property.

Тобто, не можна використати щось типу:

# zfs set sharenfs="-network 192.168.0.0/24 -network 192.168.100.0/24 -network 10.8.0.0/24" nas/nfs/backups

Але у FreeBSD 15.0 і ZFS 2.4.0 начебто розширили синтаксис, і там вже можна передати список, розділений “;“:

# zfs  sharenfs="-network 192.168.0.0/24 -maproot=root;-network 192.168.100.0/24" nas/nfs/backups

Ну а на версії 2.2.7 – просто робимо шару не через zfs set sharenfs, а напряму у файлі /etc/exports.

Відмонтовуємо шару на клієнті:

[setevoy@setevoy-work ~]  $ sudo umount /mnt/test

На сервері прибираємо sharenfs:

root@setevoy-nas:~ # zfs set sharenfs=off nas/nfs/backups

Перевіряємо:

root@setevoy-nas:~ # zfs get sharenfs nas/nfs/backups
NAME             PROPERTY  VALUE     SOURCE
nas/nfs/backups  sharenfs  off       local

Перевіряємо, що /etc/zfs/exports тепер пустий:

root@setevoy-nas:~ # cat /etc/zfs/exports
# !!! DO NOT EDIT THIS FILE MANUALLY !!!

root@setevoy-nas:~ #

Далі, редагуємо файл /etc/exports, і задаємо шари тут, кожен запис для окремої мережі:

V4: /nas/nfs
/nas/nfs/backups -network 192.168.0.0/24 -maproot=root
/nas/nfs/backups -network 192.168.100.0/24 -maproot=root
/nas/nfs/backups -network 10.8.0.0/24 -maproot=root

Перезапускаємо nfsd та mountd (mountd рестартимо явно вручну, бо були проблеми з доступом і помилки “Input/output error“):

Перевіряємо доступ з клієнта в офісі:

[setevoy@setevoy-work ~]  $ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test
[setevoy@setevoy-work ~]  $ file /mnt/test/test-client
/mnt/test/test-client: empty

І з клієнта, який вдома підключений через VPN:

[setevoy@setevoy-home ~]$ sudo wg show
interface: wg0
...
peer: xLWA/FgF3LBswHD5Z1uZZMOiCbtSvDaUOOFjH4IF6W8=
  endpoint: 178.***.***.184:51830
  allowed ips: 10.8.0.1/32, 192.168.0.0/24

З адресою 10.8.0.3:

[setevoy@setevoy-home ~]$ ip a s wg0
44: wg0: <POINTOPOINT,NOARP,UP,LOWER_UP> mtu 1420 qdisc noqueue state UNKNOWN group default qlen 1000
    link/none 
    inet 10.8.0.3/24 
    ...

Встановлюємо nfs-utils, інакше буде помилка “NFS: mount program didn’t pass remote address“:

[setevoy@setevoy-home ~]$ sudo pacman -S nfs-utils

Підключаємо шару на цьому клієнті:

[setevoy@setevoy-home ~]$ sudo mount -t nfs4 192.168.0.2:/backups /mnt/test/

Перевіряємо доступ:

[setevoy@setevoy-home ~]$ sudo touch /mnt/test/test-vpn-client

Перевіряємо:

[setevoy@setevoy-home ~]$ ls -l /mnt/test/test-vpn-client
-rw-r--r-- 1 root root 0 Dec 31 15:16 /mnt/test/test-vpn-client

І на сервері тепер бачимо два активних підключення: один Clientaddr з адреси мережі VPN – 10.8.0.3, і один з офісної – робочий ноут з 192.168.0.4:

root@setevoy-nas:~ # nfsdumpstate
Flags         OpenOwner      Open LockOwner      Lock     Deleg  OldDeleg Clientaddr
CB                    1         0         0         0         0         0 10.8.0.3   
                      0         0         0         0         0         0 192.168.0.4

Linux, /etc/fstab та systemd-automount

І наостанок додамо автомаунт, як це зроблено для Samba share.

Відключаємо шару зараз:

[setevoy@setevoy-work ~] $ sudo umount /mnt/test

Створюємо вже постійний каталог:

[setevoy@setevoy-work ~] $ sudo mkdir /nas/nfs/backups/

На клієнтах редагуємо /etc/fstab:

...

192.168.0.2:/backups  /nas/nfs/backups  nfs  sec=sys,_netdev,noauto,x-systemd.automount,nofail,noatime  0  0

Тут явно вказуємо sec=sys, тобто аутентифікацію за AUTH_SYS (по UID/GID, див. The AUTH_SYS authentication method).

Виконуємо sudo systemctl daemon-reload, перевіряємо нові unit-файли:

[setevoy@setevoy-work ~]  $ ll /run/systemd/generator/ | grep nfs
-rw-r--r-- 1 root root 177 Dec 31 15:29 nas-nfs-backups.automount
-rw-r--r-- 1 root root 274 Dec 31 15:29 nas-nfs-backups.mount

Активуємо їх:

[setevoy@setevoy-work ~]  $ sudo systemctl restart remote-fs.target

І перевіряємо доступ – шара має підключитись автоматично:

[setevoy@setevoy-work ~]  $ ll /nas/nfs/backups
total 2
-rw-r--r-- 1 setevoy nfsusers 0 Dec 31 14:18 test-client
-rw-r--r-- 1 root    root     0 Dec 31 13:12 test-server
-rw-r--r-- 1 root    nfsusers 0 Dec 31 15:16 test-vpn-client

Готово.

Корисні посилання

Loading

Arch Linux: pacman -Syu та помилки “Failed to connect to system scope bus via local transport”
0 (0)

29 Грудня 2025

Дуже рідко бувають проблеми з апгрейдами на Arch Linux, а така ситуація, як сьогодні, в мене за майже 10 років користування системою вперше.

Отже, що трапилось:

  • встановив апгрейди з pacman -Syu
  • після установки sudo reboot зависав
  • ребутнутись вдалось тільки з sudo reboot -f (force)
  • після ребуту – X.Org запустився, SDDM теж, Plasma теж – але на робочому столі все зависало і не реагувало на клаву і мишку

Дичина якась 🙂

Пофіксити вдалось, хоча так і не зрозумів що саме і чому впало.

Проблема : sudo reboot та “Failed to connect to system scope bus via local transport”

Це насправді було першим дзвіночком.

В логах при виконанні sudo reboot з’являлось повідомлення про:

...
Call to Reboot failed: Connection timed out
Failed to connect to system scope bus via local transport: Connection refused
...

Явно проблема з D-Bus/systemd, і саме тому допомогло sudo reboot -f (чи з sudo shutdown -r now, щось з них двох спрацювало), бо в такому випадку systemd не намагається координувати shutdown через system bus і напряму викликає примусовий перезапуск.

Окей – ребутнулись, і тут виникла друга – сама цікава – проблема.

KDE Plasma “висить” після апгрейду і ребуту

Беру в лапки “Plasma висить”, бо проблема все ж була не в ній – але проявлялось саме там.

Отже, ребунувся, залогінився, робочий стіл і сервіси запустились:

Але на цьому – все.

Ніякої реакції на клаву і мишку.

Перша підозра – проблема з KDE Wallet або GNOME Keyring – бо все виглядало так, що зависання виникає після їх старту.

При цьому SSH працював, і я зміг підключатись з іншого ноутбука аби подебажити і пофіксити.

Fix attempt : очистка Plasma та kwalletd

Перша спроба фіксу – прибрати все, що відноситься до Plasma і KWallet:

$ mv .cache/ .cache.bak 
$ mv .config/kwalletrc .config/kwalletrc.bak 
$ mkdir .config/plasma-bak 
$ mv .config/plasma* .config/plasma-bak/
$ mv .local/share/kwalletd/ .local/share/kwalletd.bak
$ sudo reboot

Але після ребуту і логіну система все ще висить і не реагує.

Fix attempt #2: запуск чистого X11

Далі спробував логін з чистим X11 замість Plasma – по SSH встановив:

$ sudo pacman -S --needed xorg-server xorg-xinit xterm

Хоча xorg-server і xorg-xinit вже були, бо я на X11, а не глючному Wayland.

Далі по SSH прибив поточну сесію:

$ sudo systemctl stop sddm

На ноутбуці через Alt + F2 зайшов в консоль (до речі… дивно, що це спрацювало, але ок), і запустив startx.

X11 та xterm завантажились – і тут жеж все знов перестало реагувати на клаву-мишку.

Краса…)

А отже, проблема виникає не на рівні Plasma чи конкретного desktop environment, а нижче – на рівні ядра, systemd, input stack та їх ініціалізації.

Fix attempt #3: pacman.log та “Failed to connect to system scope bus via local transport: Connection refused”

Окей – пішов дивитись логи pacman, і ось тут було дещо цікаве:

[2025-12-29T09:33:25+0200] [PACMAN] Running 'pacman -Syu --noconfirm'
[2025-12-29T09:33:25+0200] [PACMAN] synchronizing package lists
[2025-12-29T09:33:28+0200] [PACMAN] starting full system upgrade
...
[2025-12-29T09:33:49+0200] [ALPM] upgraded systemd (258.3-1 -> 259-1)
[2025-12-29T09:33:49+0200] [ALPM-SCRIPTLET] Creating group 'empower' with GID 946.
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] :: This is a systemd feature update. You may want to have a look at
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET]    NEWS for what changed, or if you observe unexpected behavior:
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET]      /usr/share/doc/systemd/NEWS
[2025-12-29T09:35:20+0200] [ALPM] upgraded polkit (126-2 -> 127-2)
[2025-12-29T09:35:20+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
...
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-catalog.hook'...
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-daemon-reload-system.hook'...
[2025-12-29T09:35:26+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:26+0200] [ALPM] running '30-systemd-daemon-reload-user.hook'...
[2025-12-29T09:35:27+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
[2025-12-29T09:35:27+0200] [ALPM] running '30-systemd-hwdb.hook'...
[2025-12-29T09:35:27+0200] [ALPM] running '30-systemd-restart-marked.hook'...
[2025-12-29T09:35:27+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
....
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET] ==> Building image from preset: /etc/mkinitcpio.d/linux.preset: 'default'
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET] ==> Using default configuration file: '/etc/mkinitcpio.conf'
[2025-12-29T09:35:30+0200] [ALPM-SCRIPTLET]   -> -k /boot/vmlinuz-linux -g /boot/initramfs-linux.img
...
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] ==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux.img'
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET]   -> Early uncompressed CPIO image generation successful
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] ==> Initcpio image generation successful
[2025-12-29T09:35:33+0200] [ALPM] running 'dbus-reload.hook'...
[2025-12-29T09:35:33+0200] [ALPM-SCRIPTLET] Failed to connect to system scope bus via local transport: Connection refused
...

І згадуємо першу проблему – що нормальний reboot не працював з тою самою помилкою підключення до системної шини (через /run/dbus/system_bus_socket).

І система не змогла виконати всі hooks, і в результаті після рестарту частина системних сервісів зависала.

Найімовірніше, це торкнулось компонентів, які залежать від udev, logind та input stack. В результаті X.Org і display manager стартували, але пристрої вводу (keyboard/mouse) опинились у неконсистентному стані, без коректної обробки подій.

Final fix: установка ядра linux-lts (але діло не в LTS)

Тут в мене з’явилась підозра, що проблема в ядрі і Intel-дайверах, і вирішив спробувати встановити LTS-ядро:

$ pacman -S linux-lts linux-lts-header

Правда, після встановлення забув зробити grub-mkconfig, а тому в меню GRUB “Advanced options for Arch Linux” нове ядро не з’явилось, і я просто загрузився зі старим.

Але… Саме після цього все запрацювало.

Чому – бо під час встановлення LTS заново запустились всі хуки, бо перший апдейт відбувався в момент оновлення systemd і D-Bus, а другий – у вже стабільному systemd-оточенні:

...
[2025-12-29T10:40:15+0200] [PACMAN] Running 'pacman -S linux-lts linux-lts-headers'
[2025-12-29T10:40:21+0200] [ALPM] transaction started
[2025-12-29T10:40:21+0200] [ALPM] installed linux-lts (6.12.63-1)
[2025-12-29T10:40:21+0200] [ALPM] installed pahole (1:1.31-1)
[2025-12-29T10:40:21+0200] [ALPM] installed linux-lts-headers (6.12.63-1)
[2025-12-29T10:40:21+0200] [ALPM] transaction completed
[2025-12-29T10:40:22+0200] [ALPM] running '30-systemd-update.hook'...
[2025-12-29T10:40:22+0200] [ALPM] running '60-depmod.hook'...
[2025-12-29T10:40:23+0200] [ALPM] running '90-mkinitcpio-install.hook'...
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Building image from preset: /etc/mkinitcpio.d/linux-lts.preset: 'default'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Using default configuration file: '/etc/mkinitcpio.conf'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> -k /boot/vmlinuz-linux-lts -g /boot/initramfs-linux-lts.img
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET] ==> Starting build: '6.12.63-1-lts'
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [base]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [udev]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [autodetect]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [microcode]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [modconf]
[2025-12-29T10:40:23+0200] [ALPM-SCRIPTLET]   -> Running build hook: [kms]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [keyboard]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [keymap]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [consolefont]
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET] ==> WARNING: consolefont: no font found in configuration
[2025-12-29T10:40:24+0200] [ALPM-SCRIPTLET]   -> Running build hook: [block]
[2025-12-29T10:40:25+0200] [ALPM-SCRIPTLET]   -> Running build hook: [encrypt]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [resume]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [filesystems]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Running build hook: [fsck]
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Generating module dependencies
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Creating zstd-compressed initcpio image: '/boot/initramfs-linux-lts.img'
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET]   -> Early uncompressed CPIO image generation successful
[2025-12-29T10:40:26+0200] [ALPM-SCRIPTLET] ==> Initcpio image generation successful
...

І після цього система запрацювала нормально.

Коротше – класичний приклад неочевидної “магії” systemd, D-Bus та системних hooks під час апгрейду.

Lessons learned

  • якщо під час pacman -Syu з’являються помилки systemd/dbus, їх не варто ігнорувати – а я звик, що апгрейди проходять без проблем, і не подивився на pacman output в консолі, а відразу почав ребутати машину
  • зависання GUI не завжди означає проблему в Destop Environment або Wayland/X11
  • повторний запуск системних hooks у стабільному середовищі може повністю виправити систему

Loading

SSH: sshd hardening на FreeBSD і Linux, та інтеграція з 1Password
0 (0)

28 Грудня 2025

Прийшов час трохи привести в порядок SSH на самому FreeBSD та на клієнтах – ноутбуках з Arch Linux, бо на домашніх машинках досі використовую парольну аутентифікацію.

Власне, описані нижче налаштування не специфічні ні для FreeBSD, ні для Linux, бо SSH server один і той самий на всіх системах (OpenSSH_9.9p2 на FreeBSD 14.3 і OpenSSH_10.2p1 на Arch Linux).

1Password користуюсь вже давно, класна інтеграція і GUI, хоча паралельно є локальний KeePassXC, в який до окремої бази періодично бекаплю дані із 1Password – див. How to export your data from the 1Password desktop app та Migrating from 1Password to KeePass, KeePassXC and KeePassium.

Це пост – частина серії по сетапу домашнього NAS на FreeBSD (див. початок у FreeBSD: Home NAS, part 1 – налаштування ZFS mirror), але вирішив його зробити окремою темою, бо тут виключно про SSH.

Що маю в поточному сетапі:

  • хост з FreeBSD/NAS: доступний тільки в локальній мережі і VPN, тому SSH brute force не очікується (але при параної або на публічно доступних серверах можна додати Fail2Ban чи SSHGuard)
  • pf firewall: як перша лінія захисту з лімітом того, звідки SSH доступний (див. FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall)
  • sshd: друга лінія захисту, базові налаштування доступу

SSH та аутентифікація по ключам

Перше і основне – це налаштувати доступ по ключам замість парольної аутентифікації.

Зробимо це, потім доступ по паролям відключимо взагалі.

На клієнті, ноутбуці з Linux, створюємо ключі:

[setevoy@setevoy-work ~] $ ssh-keygen -t ed25519 -C "setevoy@setevoy"
Generating public/private ed25519 key pair.
Enter file in which to save the key (/home/setevoy/.ssh/id_ed25519): /home/setevoy/.ssh/freebsd-nas
Enter passphrase for "/home/setevoy/.ssh/freebsd-nas" (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in /home/setevoy/.ssh/freebsd-nas
Your public key has been saved in /home/setevoy/.ssh/freebsd-nas.pub

Копіюємо на хост з FreeBSD:

[setevoy@setevoy-work ~] $ ssh-copy-id -i /home/setevoy/.ssh/freebsd-nas [email protected]
/usr/bin/ssh-copy-id: INFO: Source of key(s) to be installed: "/home/setevoy/.ssh/freebsd-nas.pub"
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
([email protected]) Password for setevoy@setevoy-nas:

Number of key(s) added: 1

Now try logging into the machine, with: "ssh -i /home/setevoy/.ssh/freebsd-nas '[email protected]'"
and check to make sure that only the key(s) you wanted were added.

Перевіряємо файл ~/.ssh/authorized_keys на FreeBSD у юзера setevoy:

root@setevoy-nas:/home/setevoy # cat .ssh/authorized_keys 
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILb2zkJzflngGx0qY71xYyHVvKI8A2GTAGTqppS0yVz2 setevoy@setevoy

Пробуємо підключення з Linux:

[setevoy@setevoy-work ~]  $ ssh -i /home/setevoy/.ssh/freebsd-nas '[email protected]'
Last login: Sat Dec 27 09:01:47 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!

...
[setevoy@setevoy-nas ~]$ 

Все працює – можна налаштувати SSH Agent.

FreeBSD та 1Password client

Тут просто для приклада – установка і підключення клієнта 1Password на FreeBSD.

Встановлюємо:

root@setevoy-nas:/home/setevoy # pkg install -y 1password-client2

Додаємо акаунт:

root@setevoy-nas:/home/setevoy # op account add
Enter your sign-in address (example.1password.com): my.1password.com            
Enter the email address for your account on my.1password.com: <ACCOUNT_EMAIL>
Enter the Secret Key for [email protected] on my.1password.com: <SECRET_KEY>
Enter the password for [email protected] at my.1password.com: 
Enter your six-digit authentication code: <OTP_CODE>
Now run 'eval $(op signin)' to sign in.

Перевіряємо акаунти:

root@setevoy-nas:/home/setevoy # op account list
SHORTHAND    URL                         EMAIL            USER ID
my           https://my.1password.com    [email protected]     7BS***KMM

Логінимось:

root@setevoy-nas:/home/setevoy # eval $(op signin)
Enter the password for [email protected] at my.1password.com:

І маємо доступ до сікретів.

Наприклад, отримати ключ, який додамо далі:

root@setevoy-nas:/home/setevoy # op item get "FreeBSD NAS SSH"
ID:          ulz***4ce
Title:       FreeBSD NAS SSH
Vault:       Personal (wb7***guq)
Created:     1 week ago
Updated:     1 week ago by Arseny
Favorite:    false
Tags:        FreeBSD,SSH
Version:     1
Category:    LOGIN
Fields:
  password:    [use 'op item get ulz***4ce --reveal' to reveal]
  username:    setevoy

Linux, 1Password та SSH Agent

Колись більш детально писав в пості SSH: RSA-ключи и ssh-agent – управление SSH-ключами и их паролями (2019 рік), але так як зараз активно користуюсь 1Password, який вміє як зберігати самі ключі – так і виступати в ролі SSH Agent.

Документація – 1Password SSH agent та Get started with 1Password for SSH.

Отримуємо приватний ключ:

[setevoy@setevoy-work ~] $ cat .ssh/freebsd-nas
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
...
vKI8A2GTAGTqppS0yVz2AAAAD3NldGV2b3lAc2V0ZXZveQECAwQFBg==
-----END OPENSSH PRIVATE KEY-----

Копіюємо, і додаємо новий ключ в 1Password (хоча він може генерувати ключі сам):

Переходимо в Settings:

Переходимо в Developer – Set up the SSH Agent:

1Password покаже зміст файлу ~/.ssh/config і навіть запропонує оновити його:

Але в мене зараз в конфігу вже є трохи налаштувань:

# GitHub.com
Host github.com
  PreferredAuthentications publickey
  IdentityFile /home/setevoy/.ssh/setevoy_main_priv_openssh

ExitOnForwardFailure yes

Тому додаємо в кінець файлу вручну.

Вказуємо два хости – nas.setevoy та, на випадок, якщо Unbound недоступний, то IP-адресу хоста з FreeBSD:

...

Host nas.setevoy 192.168.0.2
  IdentityAgent ~/.1password/agent.sock

Виконуємо ssh nas.setevoy, 1Password запросить підтвердження – пароль самого 1Password:

І тепер все працює через його SSH Agent:

[setevoy@setevoy-work ~] $ ssh nas.setevoy
...
Last login: Sat Dec 27 09:03:15 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!

...
[setevoy@setevoy-nas ~]$ 

Як працює SSH Agent

При підключені SSH-клієнт:

  • читає конфігурацію (~/.ssh/config, опції CLI)
  • підключається до SSH-агента (у нашому випадку 1Password) і отримує список доступних публічних ключів
  • по черзі пропонує серверу публічні ключі, які в нього є (з агента і, за відсутності обмежень, з ~/.ssh/)
  • sshd на сервері дивиться в ~/.ssh/authorized_keys конкретного юзера і перевіряє, чи є там запропонований публічний ключ
  • коли сервер знаходить збіг, він приймає цей публічний ключ і надсилає клієнту дані для підпису (випадковий набір чисел)
  • клієнт передає ці дані SSH-агенту, який володіє відповідним приватним ключем
  • агент створює криптографічний підпис приватним ключем і повертає його клієнту
  • сервер перевіряє підпис публічним ключем і завершує аутентифікацію

Глянути це можемо з ssh -v user@host:

[setevoy@setevoy-work ~] $ ssh -v [email protected]
debug1: OpenSSH_10.2p1, OpenSSL 3.6.0 1 Oct 2025
debug1: Reading configuration data /home/setevoy/.ssh/config
...
debug1: Connecting to nas.setevoy [192.168.0.2] port 22.
debug1: Connection established.
...
debug1: Authenticating to nas.setevoy:22 as 'setevoy'
...
debug1: Will attempt key: RTFM RSA SHA256:nedb3Qgpkxgu57MRP7/eXShHgw6N6b7SjZ3S1rNyFb4 agent
debug1: Will attempt key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
debug1: Will attempt key: /home/setevoy/.ssh/id_rsa 
debug1: Will attempt key: /home/setevoy/.ssh/id_ecdsa 
debug1: Will attempt key: /home/setevoy/.ssh/id_ecdsa_sk 
debug1: Will attempt key: /home/setevoy/.ssh/id_ed25519 ED25519 SHA256:X8L1lCBQz8Bk7K5rMGqiE+tlSthCbgaqK7ryLZ6gVWU
debug1: Will attempt key: /home/setevoy/.ssh/id_ed25519_sk 
debug1: Offering public key: RTFM RSA SHA256:nedb3Qgpkxgu57MRP7/eXShHgw6N6b7SjZ3S1rNyFb4 agent
debug1: Authentications that can continue: publickey
debug1: Offering public key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
debug1: Server accepts key: FreeBSD-NAS ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA agent
Authenticated to nas.setevoy ([192.168.0.2]:22) using "publickey".
...
Last login: Sun Dec 28 13:44:51 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

[setevoy@setevoy-nas ~]$

І для ключів “Will attempt key: RTFM RSA [...] та FreeBSD-NAS [...] як раз бачимо, що вони були отримані від agent.

SSH Agent та помилка “Too many authentication failures”

Вище бачили, що передається ціла пачка ключів, і якщо їх багато – то сервер може відкинути подальшу аутентифікацію.

Кількість спроб задається на сервері параметром MaxAuthTries (дефолт “6”) в /etc/ssh/sshd_config. Тобто, якщо маємо 7 ключів, і перші 6 не підійшли – то підключитись не зможемо.

Аби вказати SSH клієнту який конкретно приватний ключ з агента використовувати – вказуємо публічний ключ та задаємо IdentitiesOnly:

...

Host nas.setevoy 192.168.0.2
  IdentityAgent ~/.1password/agent.sock
  IdentityFile ~/.ssh/freebsd-nas.pub
  IdentitiesOnly yes

І тепер під час підключення клієнт буде передавати тільки цей ключі:

[setevoy@setevoy-work ~] $ ssh -v [email protected]
...
debug1: get_agent_identities: agent returned 2 keys
debug1: Will attempt key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
debug1: Offering public key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
debug1: Server accepts key: /home/setevoy/.ssh/freebsd-nas.pub ED25519 SHA256:ZuJ77z6BNMwra41BiTKDrJSSQrDJ0/u+4wZRCvGJMpA explicit agent
Authenticated to nas.setevoy ([192.168.0.2]:22) using "publickey".
...

Last login: Sun Dec 28 13:44:57 2025 from 192.168.0.4
FreeBSD 14.3-RELEASE (GENERIC) releng/14.3-n271432-8c9ce319fef7

Welcome to FreeBSD!
...

[setevoy@setevoy-nas ~]$

Basic SSH Server Hardening

Див. sshd_config.

Перевірити поточну конфігурацію можна з sshd -T:

root@setevoy-nas:/home/setevoy # sshd -T 
port 22
addressfamily any
listenaddress 0.0.0.0:22
...

Наприклад, чи дозволений логін root:

root@setevoy-nas:/home/setevoy # sshd -T | grep root
permitrootlogin no

Редагуємо конфіг /etc/ssh/sshd_config, задаємо мінімальні параметри, і нехай буде PermitRootLogin no вказаний тут явно.

Плюс явно вказуємо дозвіл на PubkeyAuthentication та PasswordAuthentication – парольну аутентифікацію відключимо пізніше, коли впевнимось, що все працює:

PermitRootLogin no
PubkeyAuthentication yes
PasswordAuthentication yes

Перевіряємо синтаксис з sshd -t, якщо все добре – то команда просто нічого не виведе:

root@setevoy-nas:/home/setevoy # sshd -t; echo $?
0

Виконуємо reload конфігу:

root@setevoy-nas:/home/setevoy # service sshd reload
Performing sanity check on sshd configuration.

Перевіряємо чи працює доступ з Linux:

[setevoy@setevoy-work ~]  $ ssh nas.setevoy
...
[setevoy@setevoy-nas ~]$

pf вже налаштований, див. FreeBSD: Home NAS, part 2 – знайомство з Packet Filter (PF) firewall:

...

# allow SSH from Office LAN (192.168.0.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.0.0/24 to (em0) port 22 keep state

# allow SSH from Home network (192.168.100.0/24) to FreeBSD host
pass in log on em0 proto tcp from 192.168.100.0/24 to (em0) port 22 keep state

# allow SSH from VPN clients to FreeBSD host
pass in on wg0 proto tcp from 10.8.0.0/24 to (wg0) port 22 keep state
...

Можна мережі вказати як { 192.168.0.0/24, 192.168.100.0/24, 10.8.0.0/24 }, але для SSH вирішив залишити в такому вигляді, аби було явно видно.

Далі можна додати ще трохи параметрів:

  • PermitRootLogin no: заборона підключення root, це вже є
  • PubkeyAuthentication yes: аутентифікація по ключам, теж вже є
  • PasswordAuthentication yes: пароль аутентифікація, поки залишаємо включеною
  • ChallengeResponseAuthentication no: вимикаємо keyboard-interactive аутентифікацію через PAM, яка не потрібна без 2FA
    • тут ще нюанс в тому, що навіть якщо відключити PasswordAuthentication, але ChallengeResponseAuthentication буде “yes” і при UsePAM = “yes” – то система все одно може запросити парольну аутентифікацію
  • UsePAM yes: залишаємо включеним для логування, обліку сесій, політик доступу (див. Practical Effects of Setting “UsePAM yes” on SSH in Linux)
  • AllowUsers setevoy або AllowGroups wheel: яким юзерам або групам можна підключатись по SSH
  • X11Forwarding no: блокуємо форвард X11 – графічного серверу тут все одно нема
  • AllowAgentForwarding no: забороняємо можливість використання локальних SSH-ключів клієнта з сервера через SSH-агент клієнта
  • AllowTcpForwarding no: забороняємо SSH-тунелі – на домашньому NAS це точно не треба (див. SSH-туннели в примерах – мій пост за 2013 рік, OMG… та SSH Tunnels: Secure Remote Access and Port Forwarding)
  • AuthorizedKeysFile .ssh/authorized_keys: це дефолтне значення, але задамо явно

Перевіряємо ще раз з sshd -t, виконуємо reload, перевіряємо підключення з ключем з клієнта.

Якщо все ОК – то додаємо ще трохи тюнингу:

  • PasswordAuthentication no: тепер відключаємо парольну аутентифікацію
  • AuthenticationMethods publickey: явно заборонити всі механізми окрім ключів
  • MaxSessions 2: максимальна кількість активних сесій на одне TCP-підключення
  • LoginGraceTime 30: кількість секунд, за яку клієнт має пройти аутентифікацію

Для ще більшої безпеки – можна налаштувати інший порт замість 22 (наприклад, задати Port 2222), обмежити IP, на яких слухає sshd (параметр ListenAddress 192.168.0.2), і навіть налаштувати Two-factor Authentication For SSH – але для домашнього сервера з доступом тільки з локальних мереж це вже overkill.

Loading