Лично у меня пароль на 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 больше нет секретов в открытом виде, и каждая программа получает только свои.

Это не защита от всего, но заметный шаг вперёд за минимальную цену. Если вы, как и я, отключали защиту ради удобства, рекомендую попробовать: возможно, отключать её больше не захочется.

Комментарии (1)


  1. stackshortlist
    16.10.2026 15:10

    Хорошая подборка, особенно sudo_local, который переживает обновления macOS в отличие от правки основного файла в pam.d. Связка Touch ID с ключами SSH через Secure Enclave тоже сильно снижает риск утечки при запуске чужого скрипта. А с секретами в переменных окружения вы в итоге перешли на подгрузку из связки ключей по требованию, или используете отдельный менеджер секретов?