Шифрование NFS: RPC-with-TLS как альтернатива VPN
Однажды мы задались вопросом, можно ли защитить трафик NFS-протокола. Всем известные способы, такие как VPN-туннели и различные прокси, нас не интересовали. Оказалось, недавно был опубликован RFC 9289, в котором описывается RPC-with-TLS. И мы решили разобраться, что это за зверь.
В этой статье разберёмся, как поднять шифрование для NFS-трафика с помощью RPC-with-TLS, какие есть нюансы и ограничения. Посмотрим, как настроить tls и mtls, что делать с демоном tlshd и почему важно не ставить пробелы в конфиге. Заодно проверим, как всё это работает на практике и что будет, если что-то пойдёт не так. В общем, нырнём с головой в RPC-with-TLS и посмотрим, что из этого выйдет.

Немного разъяснений
Для работы RPC-with-TLS в ядре Linux (далее просто ядро) необходимо включить две опции:
CONFIG_TLS
, которая позволяет ядру работать с TLS, но для TLS-рукопожатия (далее handshake) требуется демон tlshd;CONFIG_NET_HANDSHAKE
, появившаяся в ядре 6.4, но бэкпортированная и включённая в последних rpm-подобных дистрибутивах, таких как AlmaLinux 9 и Rocky Linux 9.
Существуют две политики безопасности для RPC-with-TLS: tls и mtls. Если кратко, то при tls происходит аутентификация только NFS-сервера, а при mtls добавляется аутентификация клиента. Отсюда вытекают следующие требования: если используется tls, то на стороне клиента корневой сертификат (CA) серверного сертификата должен быть в списке доверенных, а если используется mtls, то ещё и на стороне сервера CA клиентского сертификата должен быть в доверенных.
В Common Name (CN) серверного сертификата должно быть имя сервера, которое указывает клиент в хосте монтирования команды mount.nfs
. CN клиентского сертификата при использовании mtls для сервера не имеет значения.
Важно правильно настроить владельца и права на сертификаты и на ключи: владелец должен быть только root, права на сертификаты — 644, а на ключи — 600.
Handshake-демон tlshd
Для старта демона tlshd нужен конфигурационный файл, по умолчанию это /etc/tlshd.conf
. В этом файле очень важно, чтобы в конце строк с файлами сертификатов и ключей не было пробелов (как по мне, так это прямо «детская болезнь»), иначе демон не будет работать. Например, в ходе тестирования оказалось, что параметр выглядел так: x509.certificate=/root/my_ca/ca1/certificates/worker-1.crt
, из-за этого handshake не происходил.
Что произойдёт на клиенте с доступом в NFS-папку, если после успешного монтирования tlshd упадёт или перезапустится на клиенте или сервере? Ничего не изменится, папка останется доступной на чтение и запись. При этом шифрование трафика сохранится, так как tlshd нужен только в момент монтирования.
Чисто теоретически никто не запрещает запускать на хосте несколько экземпляров этого демона с разными конфигами. При этом в момент монтирования NFS-папки нельзя указать, какой именно экземпляр tlshd использовать для handshake в каждом случае.
Был проведён эксперимент: на клиенте запустили три экземпляра tlshd с разными конфигами (один валидный и два — нет) и стали монтировать NFS-папку с сервера. Монтирование происходило хаотично — из 10 попыток могло сработать две, или одна, или четыре, или ни одной. При каждой попытке монтирования каждый tlshd-демон писал что-то в свой лог. Это говорит о том, что они все были задействованы сразу. Отсюда вытекает ограничение: при использовании mtls может использоваться только один клиентский сертификат. Это ограничение можно обойти, если каждый раз при монтировании запускать экземпляр tlshd с нужным конфигом (читай: с нужным клиентским сертификатом) и после удачного монтирования останавливать tlshd-демон, но это скорее похоже на костыль, чем на хорошее решение.
Тестирование
Мы создали тестовый стенд и провели ручное тестирование монтирования NFS-папок с различными политиками безопасности. В результате мы убедились в шифровании NFS-трафика и посмотрели на ошибки монтирования.
Описание тестового стенда
Три виртуальные машины с Ubuntu 24.04 LTS на борту: два NFS-сервера (server-1 и server-2) с разными CA и один NFS-клиент (worker-1). На каждом NFS-сервере было экспортировано три папки /mnt/nfs_none
, /mnt/nfs_tls
, /mnt/nfs_mtls
с соответствующими политиками безопасности:
echo '/mnt/nfs_none *(rw,sync,no_subtree_check)' | sudo tee -a /etc/exports
echo '/mnt/nfs_tls *(rw,sync,no_subtree_check,xprtsec=tls)' | sudo tee -a /etc/exports
echo '/mnt/nfs_mtls *(rw,sync,no_subtree_check,xprtsec=mtls)' | sudo tee -a /etc/exports
Тестирование проводилось для NFSv3, NFSv4.1, NFSv4.2 с самоподписанными сертификатами.
Зашифрованный NFS-трафик
Вот так проверялось шифрование на NFS-сервере: tshark -i eth0 -f 'tcp and port 2049'
.
Момент монтирования папки:
1 0.000000000 10.1.1.13 → 10.1.1.11 TCP 74 674 → 2049 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 SACK_PERM TSval=398680558 TSecr=0 WS=128
2 0.000028999 10.1.1.11 → 10.1.1.13 TCP 74 2049 → 674 [SYN, ACK] Seq=0 Ack=1 Win=65160 Len=0 MSS=1460 SACK_PERM TSval=2420952483 TSecr=398680558 WS=128
3 0.000213423 10.1.1.13 → 10.1.1.11 TCP 66 674 → 2049 [ACK] Seq=1 Ack=1 Win=64256 Len=0 TSval=398680559 TSecr=2420952483
4 0.000213494 10.1.1.13 → 10.1.1.11 NFS 110 V4 NULL Call
5 0.000248597 10.1.1.11 → 10.1.1.13 TCP 66 2049 → 674 [ACK] Seq=1 Ack=45 Win=65152 Len=0 TSval=2420952483 TSecr=398680559
6 0.000331695 10.1.1.11 → 10.1.1.13 NFS 102 V4 NULL Reply (Call In 4)
7 0.000461213 10.1.1.13 → 10.1.1.11 TCP 66 674 → 2049 [ACK] Seq=45 Ack=37 Win=64256 Len=0 TSval=398680559 TSecr=2420952483
8 0.015183491 10.1.1.13 → 10.1.1.11 TLSv1 417 Client Hello (SNI=server-1)
9 0.015853138 10.1.1.11 → 10.1.1.13 TLSv1.3 264 Server Hello, Change Cipher Spec
10 0.016537570 10.1.1.13 → 10.1.1.11 TLSv1.3 72 Change Cipher Spec
11 0.029186413 10.1.1.11 → 10.1.1.13 TLSv1.3 7306 Application Data
12 0.029199606 10.1.1.11 → 10.1.1.13 TCP 7306 2049 → 674 [PSH, ACK] Seq=7475 Ack=402 Win=64896 Len=7240 TSval=2420952512 TSecr=398680575 [TCP segment of a reassembled PDU]
13 0.029770565 10.1.1.13 → 10.1.1.11 TCP 66 674 → 2049 [ACK] Seq=402 Ack=7475 Win=78592 Len=0 TSval=398680588 TSecr=2420952512
14 0.029770658 10.1.1.13 → 10.1.1.11 TCP 66 674 → 2049 [ACK] Seq=402 Ack=14715 Win=92160 Len=0 TSval=398680588 TSecr=2420952512
15 0.029790811 10.1.1.11 → 10.1.1.13 TLSv1.3 2255 Application Data, Application Data, Application Data, Application Data
16 0.029978228 10.1.1.13 → 10.1.1.11 TCP 66 674 → 2049 [ACK] Seq=402 Ack=16904 Win=96512 Len=0 TSval=398680588 TSecr=2420952512
17 0.046021972 10.1.1.13 → 10.1.1.11 TLSv1.3 1923 Application Data, Application Data, Application Data
18 0.046070545 10.1.1.11 → 10.1.1.13 TCP 66 2049 → 674 [ACK] Seq=16904 Ack=2259 Win=63104 Len=0 TSval=2420952529 TSecr=398680604
19 0.049060919 10.1.1.13 → 10.1.1.11 TLSv1.3 132 Application Data
20 0.049120160 10.1.1.11 → 10.1.1.13 TLSv1.3 116 Application Data
21 0.049489753 10.1.1.13 → 10.1.1.11 TLSv1.3 348 Application Data
22 0.049569299 10.1.1.11 → 10.1.1.13 TLSv1.3 192 Application Data
...
Момент записи строки в файл, расположенный в NFS-папке:
82 24.007203648 10.1.1.13 → 10.1.1.11 TLSv1.3 272 Application Data
83 24.007358680 10.1.1.11 → 10.1.1.13 TLSv1.3 260 Application Data
84 24.007529719 10.1.1.13 → 10.1.1.11 TCP 66 792 → 2049 [ACK] Seq=8269 Ack=23090 Win=94976 Len=0 TSval=398803883 TSecr=2421075807
85 24.007641431 10.1.1.13 → 10.1.1.11 TLSv1.3 372 Application Data
86 24.007724635 10.1.1.11 → 10.1.1.13 TLSv1.3 468 Application Data
87 24.007999216 10.1.1.13 → 10.1.1.11 TLSv1.3 316 Application Data
88 24.011385927 10.1.1.11 → 10.1.1.13 TLSv1.3 276 Application Data
89 24.011745229 10.1.1.13 → 10.1.1.11 TLSv1.3 296 Application Data
90 24.011833565 10.1.1.11 → 10.1.1.13 TLSv1.3 268 Application Data
91 24.052731556 10.1.1.13 → 10.1.1.11 TCP 66 792 → 2049 [ACK] Seq=9055 Ack=23904 Win=94976 Len=0 TSval=398803929 TSecr=2421075812
Ошибки монтирования
Если при монтировании у клиента не будет CA серверного сертификата в доверенных, то ошибка будет такой:
# mount.nfs -o xprtsec=mtls,nfsvers=4.2 server-1:/mnt/nfs_mtls /mnt/nfs_mtls
mount.nfs: access denied by server while mounting server-1:/mnt/nfs_mtls
Интересная особенность
На NFS-сервере есть папка, которая экспортируется по mtls. Если клиент при монтировании укажет политику tls и версию NFS-протокола 3, то монтирование произойдёт успешно, но доступа в папку не будет.
Монтируем:
# mount.nfs -o xprtsec=tls,nfsvers=3 server-1:/mnt/nfs_mtls /mnt/nfs_mtls
#
Доступа нет:
# ls -l /mnt/nfs_mtls/
ls: cannot open directory '/mnt/nfs_mtls/': Permission denied
С NFS 4.1 и 4.2 такого поведения нет — папка просто не монтируется:
# for i in 4.1 4.2;do mount.nfs -o xprtsec=tls,nfsvers=$i server-1:/mnt/nfs_mtls /mnt/nfs_mtls ;done
mount.nfs: Operation not permitted for server-1:/mnt/nfs_mtls on /mnt/nfs_mtls
mount.nfs: Operation not permitted for server-1:/mnt/nfs_mtls on /mnt/nfs_mtls
Ограничение для одного NFS-сервера
Если на одном и том же клиенте монтировать несколько NFS-папок по tls или mtls (в примере ниже используется mtls) с одного и того же NFS-сервера, то всё монтируется без проблем:
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2,xprtsec=mtls server-1:/mnt/nfs_mtls/$i /mnt/nfs_mtls/$i ;done
#
При этом handshake произойдёт только один раз — при монтировании первой папки. В логе tlshd это видно так:
tlshd[7776]: handshake with server-1 (10.1.1.11) was successful
После этого нельзя будет смонтировать с того же NFS-сервера другую папку по tls (или mtls, если первый раз монтирование было по tls).
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2,xprtsec=tls server-1:/mnt/nfs_tls/$i /mnt/nfs_tls/$i ;done
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/11 on /mnt/nfs_tls/11
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/22 on /mnt/nfs_tls/22
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/33 on /mnt/nfs_tls/33
Самое интересное, что при этом handshake пройдёт успешно:
tlshd[7807]: handshake with server-1 (10.1.1.11) was successful
tlshd[7815]: handshake with server-1 (10.1.1.11) was successful
tlshd[7820]: handshake with server-1 (10.1.1.11) was successful
А вот монтировать папки без RPC-with-TLS можно без проблем:
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2 server-1:/mnt/nfs_none/$i /mnt/nfs_none/$i ;done
#
Но если изменить порядок монтирования — сначала без RPC-with-TLS, а потом с любыми политиками RPC-with-TLS, то монтирование с RPC-with-TLS не произойдёт:
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2 server-1:/mnt/nfs_none/$i /mnt/nfs_none/$i ;done
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2,xprtsec=tls server-1:/mnt/nfs_tls/$i /mnt/nfs_tls/$i ;done
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/11 on /mnt/nfs_tls/11
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/22 on /mnt/nfs_tls/22
mount.nfs: Operation not permitted for server-1:/mnt/nfs_tls/33 on /mnt/nfs_tls/33
# for i in 11 22 33;do mount.nfs -o nfsvers=4.2,xprtsec=mtls server-1:/mnt/nfs_mtls/$i /mnt/nfs_mtls/$i ;done
mount.nfs: Operation not permitted for server-1:/mnt/nfs_mtls/11 on /mnt/nfs_mtls/11
mount.nfs: Operation not permitted for server-1:/mnt/nfs_mtls/22 on /mnt/nfs_mtls/22
mount.nfs: Operation not permitted for server-1:/mnt/nfs_mtls/33 on /mnt/nfs_mtls/33
Отсюда получается ограничение: с одного и того же NFS-сервера нельзя одновременно монтировать папки, перемешивая политики безопасности tls, mtls и без шифрования.
Заключение
RPC-with-TLS — это интересный способ защиты трафика NFS, но со своими ограничениями. Например, нельзя одновременно монтировать папки с разными политиками безопасности (tls, mtls и без шифрования) с одного сервера. А ещё при использовании mtls можно задействовать только один клиентский сертификат. Тем не менее шифрование работает стабильно, и даже если демон tlshd упадёт после монтирования, доступ к папке и защита трафика сохранятся.
Единственная нативная альтернатива — это Kerberos в режиме krb5p, но у него есть свои минусы: сложная настройка аутентификации пользователей и сильное падение производительности.
Кстати, в Deckhouse Kubernetes Platform в модуле csi-nfs, начиная с версии v0.2.0, уже есть поддержка RPC-with-TLS.
P. S.
Читайте также в нашем блоге:
Извлекаем файлы из образа повреждённого диска: ddrescue, losetup и немного магии
Как Linux готовится ко сну
Как собрать Linux-контейнер с нуля и без Docker
Habrahabr.ru прочитано 4144 раза