Лично у меня пароль на sudo был отключён, SSH-ключи были незашифрованы, а секреты для переменных окружения хранились открытым текстом в ~/.zshrc, потому что я не хочу целыми днями вводить пароли. Уверен, я не один такой.
При этом компьютер разработчика — довольно уязвимое место. Я постоянно ставлю пакеты непонятно от кого, запускаю чужие программы и скрипты. Достаточно одного неосторожного запуска, и кто-то соберёт все мои SSH-ключи и секреты из переменных окружения, получит права суперпользователя, а я этого даже не замечу.
Давно хотелось как-то защитить секреты, не жертвуя удобством. Оказалось, что Touch ID отлично в этом помогает. Рассказываю как.
1. Интеграция с sudo
Я ни разу не видел пароль на sudo на Linux-сервере, поэтому и у себя на маке сразу же отключил его по привычке. Но это было необязательно: в macOS уже есть всё для настройки sudo с Touch ID из коробки.
Начиная с macOS 14 Sonoma, Apple заботливо положили пример в файл /etc/pam.d/sudo_local.template:
# sudo_local: local config file which survives system update and is included for sudo # uncomment following line to enable Touch ID for sudo #auth sufficient pam_tid.so
Достаточно просто скопировать его в /etc/pam.d/sudo_local:
sudo cp /etc/pam.d/sudo_local.template /etc/pam.d/sudo_local
И раскомментировать последнюю строку, чтобы получилось так:
# sudo_local: local config file which survives system update and is included for sudo # uncomment following line to enable Touch ID for sudo auth sufficient pam_tid.so
Теперь sudo будет запрашивать подтверждение через Touch ID. Если Touch ID не распознает отпечаток или вы нажмёте «Отмена», sudo попросит пароль, как раньше.
По умолчанию sudo запоминает авторизацию на 5 минут. Таймаут можно уменьшить или отключить совсем: добавьте Defaults timestamp_timeout=0 через visudo.
Поддержка tmux и screen
Внутри tmux и screen Touch ID не сработает: окно Touch ID может показать только процесс из GUI-сессии пользователя, а сервер tmux работает как демон. Можно это исправить установив PAM-модуль, который переподключает процесс к GUI-сессии пользователя:
brew install pam-reattach
И добавить его в sudo_local перед pam_tid.so. Итоговый sudo_local должен выглядеть так:
auth optional /opt/homebrew/lib/pam/pam_reattach.so auth sufficient pam_tid.so
2. Хранение SSH-ключей в Secure Enclave
Второе место, где я не хочу каждый раз вводить пароль, — подключение по SSH. Было бы удобно расшифровывать ключ с помощью Touch ID, но есть решение даже лучше. Уже давно все маки имеют Secure Enclave — аппаратное криптографическое хранилище, в котором можно хранить в том числе и приватные SSH-ключи. Начиная с macOS 26 Tahoe, уже всё готово, чтобы этим пользоваться.
Создаём ключ прямо в Secure Enclave:
sc_auth create-ctk-identity -l user@server -k p-256-ne -t bio
Смотрим список ключей:
sc_auth list-ctk-identities
Появится список в таком виде:
Key Type Public Key Hash Prot Label Common Name Email Address Valid To Valid p-256-ne 18F409BAEA5400C276C6551409F9C6111697CA7B bio user@server user@server 09.09.2027, 22:41 YES
Может смутить столбик «Valid To» и возникнуть вопрос: мой ключ протухнет через год?
Ответ: нет
«Valid To» относится к самоподписанному сертификату X.509, который sc_auth автоматически создаёт и привязывает к CTK-идентичности. Сам приватный ключ остаётся в Secure Enclave. В обсуждении нативной поддержки Secure Enclave в SSH это прямо подтверждают: сертификат используется для TLS, клиентской аутентификации и S/MIME, а SSH его не использует.
Поэтому для SSH срок «Valid To» не важен.
Теперь нужно получить публичный ключ. Это можно сделать двумя способами.
Способ 1. Экспортируем публичный ключ в файл:
cd ~/.ssh ssh-keygen -w /usr/lib/ssh-keychain.dylib -K -N ""
Когда появится запрос PIN, оставьте его пустым.
В папке появятся два файла:
id_ecdsa_sk_rk.pub— публичный ключ, который можно положить вauthorized_keysна серверах;id_ecdsa_sk_rk— хоть и выглядит как приватный ключ, на самом деле содержит только данные для обращения к аппаратному ключу: его идентификатор и параметры провайдера. Скопировать этот файл на другую машину и подключиться с ним не получится.
Способ 2. Посмотреть публичный ключ без создания файлов можно, добавив его в SSH-агент.
Удаляем из агента все лишние ключи:
ssh-add -D
Добавляем в агент ключи из Secure Enclave:
ssh-add -K -S /usr/lib/ssh-keychain.dylib
Смотрим публичные ключи в агенте:
ssh-add -L
Настраиваем SSH
Чтобы SSH-клиент начал использовать ключи из Secure Enclave, нужно указать ему провайдер SecurityKeyProvider и файл ключа IdentityFile.
Это можно сделать в файле ~/.ssh/config:
Host * SecurityKeyProvider /usr/lib/ssh-keychain.dylib IdentityFile ~/.ssh/id_ecdsa_sk_rk
или добавив строку в ~/.zprofile:
export SSH_SK_PROVIDER=/usr/lib/ssh-keychain.dylib
Теперь при подключении по SSH появится запрос Touch ID.
Важно помнить, что ключ из Secure Enclave нельзя скопировать даже для бэкапа. Позаботьтесь о запасном доступе к серверам на случай, если с вашим маком что-то случится.
Подробнее об этом можно почитать здесь.
Сервер должен быть не старее OpenSSH 8.2 (февраль 2020).
Как проверить
Проверить сервер можно так:
ssh -V ssh -Q key | grep sk-ecdsa
Если вторая команда ничего не вывела, этот OpenSSH не знает такой тип ключей.
Даже на свежем сервере тип ключа может быть отключён в sshd_config через PubkeyAcceptedAlgorithms (в версиях до 8.5 — PubkeyAcceptedKeyTypes). Проверить итоговые настройки sshd можно так:
sudo sshd -T | grep -i pubkeyaccepted
В списке должен быть sk-ecdsa-sha2-nistp256@openssh.com.
Если у вас macOS старше Tahoe, в качестве альтернативы посмотрите на эти проекты:
3. Защита секретов в переменных окружения
У меня много секретов в переменных окружения, и все они прописаны открытым текстом в ~/.zshrc. Это ключи от разных Docker Registry, токены облачных провайдеров, токен GitHub, ключи шифрования Ansible Vault и т. д.
Меня всегда смущало, что любой процесс может прочитать их все, а не только те, которые ему нужны. Но в документации часто предлагают добавить секрет именно в ~/.zshrc.
Хотелось бы удобно выдавать каждому приложению только нужные ему секреты и вообще не хранить их на диске в открытом виде.
Консольная утилита от 1Password использует изящное решение: программа запускается через обёртку, которая подставляет секреты в переменные окружения только запускаемого процесса.
Как это работает?
Создаём файл .env файл который хранит ссылки на секреты:
DATABASE_URL=op://Development/PostgreSQL/connection_string API_TOKEN=op://Development/MyService/credential
Программу, которой нужны секреты, запускаем не напрямую, а через op run:
op run --env-file=.env -- myapp --my-flags
На диске не хранится ни одного секрета, а программа получает только те, которые ей нужны.
Почему бы не написать такую же обёртку самому? Выглядит просто.
И да, эта идея уже много кому пришла в голову до меня. Я нашёл множество проектов, которые реализуют в точности такое поведение на всех возможных языках, но почему-то все эти проекты непопулярны.
Я попробовал всё, что смог найти, и больше всего мне понравился envrcctl на Python. Его я и буду использовать в качестве примера.
Подставляем секреты через envrcctl exec
Устанавливаем:
brew install rioriost/tap/envrcctl
Добавляем секрет:
envrcctl secret set ANSIBLE_VAULT_PASSWORD --account myproject:ansible_vault
Проверяем:
envrcctl exec -- printenv ANSIBLE_VAULT_PASSWORD
envrcctl запросит Touch ID, достанет секрет из Keychain и передаст его printenv. Если всё хорошо printenv напечатает пароль.
Пример запуска ansible:
envrcctl exec -- ansible-playbook playbooks/basic.yml --diff --check
Загружаем нужные секреты в текущий shell
Если запускать команды через envrcctl exec неудобно, секреты можно экспортировать в shell. Так менее безопасно, потому что все переменные увидит любой процесс, запущенный из этого shell. Но это всё равно лучше, чем хранить пароли в ~/.zshrc.
Сам envrcctl не может добавить переменные окружения в текущий shell: в Unix дочерний процесс не может менять переменные окружения родителя. Зато он умеет печатать их в формате export VAR=VALUE, поэтому envrcctl inject нужно вызвать через eval или source:
eval "$(envrcctl inject --shell)"
или
source <(envrcctl inject --shell)
Чтобы каждый раз не писать такую длинную команду мы можем создать функцию в ~/.zshrc и использовать её:
senv() { eval "$(envrcctl inject --shell)" }
Теперь для экспорта секретов в текущий shell достаточно написать senv и запускать программы как раньше.
Автоматически загружаем секреты при входе в папку
Можно ещё проще!
Есть популярная утилита direnv, которая автоматически загружает переменные окружения в shell при входе в папку с файлом .envrc. envrcctl умеет с ней интегрироваться.
Устанавливаем:
brew install direnv
Добавляем хук в ~/.zshrc:
eval "$(direnv hook zsh)"
Создаём новый .envrc:
envrcctl init --inject
Разрешаем direnv работать в этой папке:
direnv allow
Перезапускаем терминал, чтобы применился хук.
Теперь при переходе в папку сразу появится запрос Touch ID, и в shell добавятся нужные переменные окружения из Keychain.
Заключение
На каждый из трёх способов уходит 5–10 минут. Нужно только то, что уже есть в macOS, и пара утилит из brew. Привычная работа почти не меняется: вместо ввода пароля достаточно коснуться Touch ID.
Что изменилось:
sudo больше не срабатывает молча. Любой скрипт или агент, которому понадобились права root, упрётся в запрос Touch ID.
Приватный SSH-ключ не украсть, скопировав файл: он никогда не покидает Secure Enclave.
В
~/.zshrcбольше нет секретов в открытом виде, и каждая программа получает только свои.
Это не защита от всего, но заметный шаг вперёд за минимальную цену. Если вы, как и я, отключали защиту ради удобства, рекомендую попробовать: возможно, отключать её больше не захочется.
stackshortlist
Хорошая подборка, особенно sudo_local, который переживает обновления macOS в отличие от правки основного файла в pam.d. Связка Touch ID с ключами SSH через Secure Enclave тоже сильно снижает риск утечки при запуске чужого скрипта. А с секретами в переменных окружения вы в итоге перешли на подгрузку из связки ключей по требованию, или используете отдельный менеджер секретов?