Закрытие беспарольного доступа к PostgreSQL в Kubernetes#

В образе PostgreSQL, из которого операторы развертывают СУБД средствами платформы (managed), файл pg_hba.conf содержал правило host all all 0.0.0.0/0 trust. PostgreSQL обрабатывает правила pg_hba.conf по первому совпадению, поэтому это правило перекрывало корректное правило md5, расположенное ниже, и оно не применялось.

В результате любой под кластера, а также любой узел с сетевым доступом к поду PostgreSQL, мог подключиться к СУБД по TCP под любой ролью, включая суперпользователя postgres, без пароля. Пароль суперпользователя при этом задан, но правилом trust игнорировался.

Исправление внесено в образ PostgreSQL начиная с версии 2.1 и применяется автоматически только при чистой установке из этого образа. В установках из более ранних образов, в том числе после чистой установки, а также на развернутых ранее экземплярах устранение выполняется вручную по приведенной ниже процедуре.

Важно

Обновление платформы до версии 2.1 не устраняет проблему на уже развернутых экземплярах. Подробности приведены в описании устранения при обновлении платформы.

Кого это касается#

Процедура относится к установкам, в которых PostgreSQL развернута средствами платформы: под СУБД создается оператором как объект StatefulSet с названием вида *-postgres-15, а в манифесте компонента задан параметр type: "managed".

Как правило, в установке присутствуют два таких экземпляра, и исправить необходимо каждый из них:

  • общая база данных компонентов – <deployment>-postgres-15, например aa-demo-postgres-15;

  • база данных панели мониторинга – <dashboard>-postgres-15, например aa-demo-dashboard-postgres-15.

Здесь:

  • <deployment> – название развертывания платформы, заданное в манифестах;

  • <dashboard> – название ресурса Dashboard.

Установки с внешней СУБД PostgreSQL (type: "unmanaged") этим образом не управляются, и процедура на них не распространяется.

Проверка экземпляра#

Для проверки экземпляра выполните следующие действия:

  1. Получите список всех экземпляров PostgreSQL, развернутых средствами платформы:

    kubectl get sts -A --no-headers -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name \
       | grep -- -postgres-15
    

    Каждая строка вывода – отдельный экземпляр. Экземпляров может быть несколько: помимо основного, собственный экземпляр разворачивает панель мониторинга.

  2. Задайте переменные окружения для проверяемого экземпляра, подставив значения из вывода предыдущей команды:

    NS=<namespace>
    POD=<statefulset>-0
    

    Здесь:

    • <namespace> – пространство имен из первого столбца вывода;

    • <statefulset> – название объекта StatefulSet из второго столбца.

    Важно

    Название пода нельзя собирать из названия развертывания платформы: экземпляр панели мониторинга называется иначе. Проверку и устранение необходимо выполнить для каждой строки вывода и убедиться, что необработанных экземпляров не осталось.

  3. Выведите действующий файл pg_hba.conf:

    kubectl exec -n $NS $POD -- sh -c 'grep -nEv "^[[:space:]]*#|^[[:space:]]*$" "$(psql -tAc "SHOW hba_file")"'
    

    Примечание

    Путь к файлу необходимо получать от самого сервера с помощью запроса SHOW hba_file, а не из переменной окружения PGDATA: в этом образе она указывает на другой каталог.

    Вывод показывает файл целиком и нужен для ручной правки, а само наличие уязвимости определяет сервер:

    kubectl exec -i -n $NS $POD -- psql -tA -F' | ' <<'SQL'
    SELECT line_number, type, database, user_name, address, auth_method
    FROM pg_hba_file_rules
    WHERE auth_method = 'trust' AND type <> 'local'
    ORDER BY line_number;
    SQL
    

    Представление pg_hba_file_rules отдает правила так, как их разобрал сам сервер, поэтому запрос видит и строки, записанные в нестандартном виде, например перенесенные обратной косой чертой. Экземпляр уязвим, если запрос вернул хотя бы одну строку, например такую:

    82 | host | {all} | {all} | 0.0.0.0 | trust
    
  4. Дополнительно проверьте подключение суперпользователя по TCP с заведомо неверным паролем:

    PODIP=$(kubectl get pod -n $NS $POD -o jsonpath='{.status.podIP}')
    kubectl exec -n $NS $POD -- sh -c "PGPASSWORD=wrong psql -h $PODIP -U postgres -d postgres -tAc 'select 1'"
    

    Ожидаемый результат:

    • на уязвимом экземпляре команда возвращает 1;

    • на исправленном экземпляре команда завершается ошибкой FATAL: password authentication failed for user "postgres".

Устранение на развернутом кластере#

Правка вносится в файл pg_hba.conf и активируется перезагрузкой конфигурации без перезапуска СУБД: соединения не разрываются, простоя нет.

Предупреждение

Перезагрузка конфигурации закрывает доступ всем ролям, у которых пароль не задан. Для СУБД, развернутой средствами платформы, пароли заданы по умолчанию: пароли компонентов задает сценарий инициализации оператора, а пароль postgres берется из секрета. Роль, заведенная вручную без пароля, доступ потеряет. Правка затрагивает все правила подключений по TCP, в том числе для петлевых адресов 127.0.0.1/32 и ::1/128, если они есть в файле, поэтому после нее пароль требуется при любом подключении по TCP, включая подключение изнутри пода с параметром -h. Команды и сценарии, которые подключаются к СУБД по названию сервиса или по адресу пода, необходимо запускать с паролем, например переданным через файл .pgpass. Подключения через локальный сокет без параметра -h, которыми пользуются команды на этой странице, правка не затрагивает.

Для устранения проблемы выполните следующие действия для каждого экземпляра, полученного на шаге проверки:

  1. Задайте переменные окружения:

    NS=<namespace>
    POD=<statefulset>-0
    
  2. Убедитесь, что у всех ролей с правом входа заданы пароли:

    kubectl exec -n $NS $POD -- psql -tAc \
       "SELECT rolname FROM pg_authid WHERE rolcanlogin AND rolpassword IS NULL"
    

    Ожидаемый результат: пустой вывод. Если команда вернула названия ролей, задайте им пароли до перезагрузки конфигурации – иначе эти роли потеряют доступ.

  3. Замените правила trust для подключений по TCP и перезагрузите конфигурацию:

    kubectl exec -i -n $NS $POD -- sh <<'EOF'
    HBA=$(psql -tAc "SHOW hba_file") || exit 2
    [ -n "$HBA" ] && [ -f "$HBA" ] || { echo "ERROR: файл pg_hba.conf не прочитан"; exit 2; }
    cp "$HBA" "$HBA.trust-fix.bak" || { echo "ERROR: резервная копия не создана, файл не изменен"; exit 2; }
    sed -ri 's@^([[:space:]]*host(ssl|nossl|gssenc|nogssenc)?[[:space:]]+[^#]*[[:space:]])trust([[:space:]]*(#.*)?)$@\1md5\3@' "$HBA"
    psql -tAc "SELECT pg_reload_conf()" >/dev/null || { echo "ERROR: конфигурация не перезагружена"; exit 2; }
    echo "reload OK"
    psql -tA -F' | ' -c "SELECT line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE auth_method = 'trust' AND type <> 'local' ORDER BY line_number"
    EOF
    

    Команда последовательно выполняет следующие действия:

    • прекращает работу, если не получила путь к файлу или не создала резервную копию, поэтому файл в этих случаях остается нетронутым;

    • сохраняет резервную копию исходного файла рядом с ним под названием pg_hba.conf.trust-fix.bak;

    • заменяет trust на md5 во всех строках подключений по TCP – host, hostssl, hostnossl, hostgssenc и hostnogssenc, – включая строки с комментарием в конце;

    • не изменяет строки local, которые нужны для администрирования внутри пода, и не трогает текст комментариев;

    • применяет изменения без перезапуска СУБД;

    • спрашивает у сервера, не осталось ли правил trust для подключений по TCP.

    Ожидаемый результат: сообщение reload OK, после которого запрос не вернул ни одной строки.

    Важно

    Выражение sed правит строки в обычной записи, а последний запрос показывает правила так, как их разобрал сервер. Поэтому строку, записанную в нестандартном виде, например перенесенную обратной косой чертой, команда не преобразует, но покажет. Исправьте такие строки вручную и повторите перезагрузку конфигурации.

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

  • файл pg_hba.conf находится в каталоге PGDATA на постоянном томе PVC, поэтому правка сохраняется на диске;

  • под создает файл pg_hba.conf заново только при первичной инициализации, то есть на пустом томе;

  • оператор не изменяет содержимое файла pg_hba.conf: он управляет только объектом StatefulSet и словарем конфигурации инициализации, создающим базы данных и роли.

Устранение при обновлении до 2.1#

Обновление платформы до версии 2.1 само по себе не изменяет файл pg_hba.conf на существующих экземплярах. Порядок устранения проблемы тот же, что и на развернутом кластере.

Причины следующие:

  • Исправление в образе меняет только поведение утилиты initdb, а она отрабатывает лишь при первичной инициализации пустого тома. При обновлении том PVC сохраняется, поэтому при запуске нового образа инициализация базы данных пропускается, и прежний файл pg_hba.conf остается без изменений.

  • Мажорная версия PostgreSQL между версиями 2.0-upd2 и 2.1 не меняется – в обеих используется PostgreSQL 15. Поэтому обновление сводится к перезапуску пода на новом образе, а конфигурация не затрагивается.

  • При мажорном обновлении PostgreSQL конфигурацию нового кластера приводят в соответствие с прежней вручную, поэтому уязвимые правила могут быть перенесены вместе с ней. Проверку экземпляра необходимо выполнять и после такого обновления.

Таким образом, после обновления до версии 2.1 исправленный образ уже установлен, но действующий файл pg_hba.conf на ранее развернутых экземплярах не изменяется. Автоматически исправление применяется только при чистой установке, то есть на новом томе PVC.

Рекомендуемый порядок действий при обновлении:

  1. Выполните обновление платформы штатным способом.

  2. Выполните устранение проблемы на всех экземплярах PostgreSQL, развернутых средствами платформы.

  3. Выполните проверку результата.

Проверка результата#

Для проверки результата выполните следующие действия для каждого экземпляра:

  1. Убедитесь, что у сервера не осталось действующего правила trust для подключений по TCP:

    kubectl exec -i -n $NS $POD -- sh <<'EOF'
    BAD=$(psql -tAc "SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL") || exit 2
    [ "$BAD" = "0" ] || { echo "ERROR: сервер не разобрал $BAD строк файла, проверка не выполнена"; exit 2; }
    ROWS=$(psql -tA -F' | ' -c "SELECT line_number, type, database, user_name, address, auth_method FROM pg_hba_file_rules WHERE auth_method = 'trust' AND type <> 'local' ORDER BY line_number") || exit 2
    if [ -n "$ROWS" ]; then
       echo "$ROWS"
       echo "FAIL: остались правила trust для подключений по TCP"
       exit 1
    fi
    echo "OK: правил trust для подключений по TCP не осталось"
    EOF
    

    Проверка спрашивает свойство у самого сервера, а не разбирает текст файла, поэтому она видит и правила, записанные в нестандартном виде. Представление pg_hba_file_rules отражает содержимое файла, а не загруженную сервером конфигурацию, поэтому, если файл правили вручную, перед проверкой выполните перезагрузку конфигурации запросом SELECT pg_reload_conf(). Возможны три исхода:

    • OK – сервер разобрал файл целиком, и правил trust для подключений по TCP в нем нет;

    • FAIL со списком правил – такие правила остались, поэтому исправьте их вручную и повторите перезагрузку конфигурации;

    • ERROR (код возврата 2) – проверка не выполнена, потому что запрос не прошел или сервер не разобрал часть строк файла. Проверьте название пода и пространства имен, а также права на выполнение kubectl exec, и повторите проверку. Отсутствие сообщения OK нельзя трактовать как отсутствие проблемы.

  2. Убедитесь, что беспарольное подключение суперпользователя по TCP отклоняется:

    PODIP=$(kubectl get pod -n $NS $POD -o jsonpath='{.status.podIP}')
    OUT=$(kubectl exec -n $NS $POD -- sh -c "PGPASSWORD=wrong psql -h $PODIP -U postgres -d postgres -tAc 'select 1'" 2>&1)
    if printf '%s\n' "$OUT" | grep -q 'authentication failed'; then
       echo "OK: пароль требуется"
    elif printf '%s\n' "$OUT" | grep -qx '1'; then
       echo "FAIL: подключение без пароля прошло"
    else
       echo "ERROR: проверка не выполнена: $OUT"
    fi
    

    Команда ищет результат по строкам вывода, поэтому сообщения, которые psql дописывает в поток ошибок, не меняют исход.

  3. Убедитесь, что компоненты платформы продолжают работать, так как они подключаются к СУБД по TCP с паролем:

    1. Выведите поды пространства имен, которые не готовы к работе:

      kubectl get pods -n $NS --no-headers \
         | awk '$3 != "Completed" { split($2, r, "/"); if ($3 != "Running" || r[1] != r[2]) print }'
      

      Команда выводит под, если он не завершил работу штатно (Completed) и при этом не находится в состоянии Running или готовы не все его контейнеры, например 0/1 в столбце READY. Под, у которого проба готовности не проходит из-за ошибки аутентификации в СУБД, остается в состоянии Running с неполной готовностью, поэтому команда его выводит.

      Ожидаемый результат: пустой вывод.

    2. Найдите в журналах подов ошибки аутентификации в СУБД:

      for p in $(kubectl get pods -n $NS -o name --field-selector=status.phase=Running); do
         kubectl logs -n $NS "$p" --all-containers --since=15m 2>/dev/null \
            | grep -iE 'password authentication failed|no password supplied' \
            | sed "s|^|$p: |"
      done
      

      Команда просматривает журналы за последние 15 минут, поэтому, если перезагрузка конфигурации выполнена раньше, увеличьте значение параметра --since.

      Ожидаемый результат: пустой вывод.

    Возможны следующие исходы:

    • оба вывода пустые – компоненты работают с паролями, и правку можно оставить;

    • первая команда вывела поды, а вторая ничего не нашла – поды еще запускаются, поэтому дождитесь их готовности и повторите обе команды;

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

Откат#

Изменение применяется перезагрузкой конфигурации, без перезапуска СУБД и без разрыва соединений, поэтому простоя нет. Резервная копия исходного файла сохраняется рядом с ним под названием pg_hba.conf.trust-fix.bak.

Для отката изменений выполните следующую команду:

kubectl exec -i -n $NS $POD -- sh <<'EOF'
HBA=$(psql -tAc "SHOW hba_file") || exit 2
[ -n "$HBA" ] || { echo "ERROR: путь к файлу не получен, откат не выполнен"; exit 2; }
[ -f "$HBA.trust-fix.bak" ] || { echo "ERROR: резервная копия не найдена, откат не выполнен"; exit 2; }
cp "$HBA.trust-fix.bak" "$HBA" || { echo "ERROR: файл не восстановлен"; exit 2; }
psql -tAc "SELECT pg_reload_conf()" >/dev/null || { echo "ERROR: файл восстановлен, но конфигурация не перезагружена"; exit 2; }
echo "rollback OK"
EOF

Сообщение rollback OK команда печатает только после восстановления файла и перезагрузки конфигурации. Каждый предыдущий шаг прекращает работу с кодом возврата 2, поэтому отсутствие резервной копии команда сообщает как ошибку, а не как успех. Этот путь нужен, когда компоненты уже не поднялись, и администратору здесь важен верный ответ.

Дополнительные замечания:

  • Метод md5 в файле pg_hba.conf совместим с паролями, которые хранятся в формате scram-sha-256: при таком пароле PostgreSQL автоматически применяет механизм SCRAM. Поэтому существующие пароли компонентов продолжают работать без изменений.

  • В качестве дополнительной меры защиты рекомендуется ограничить сетевой доступ к поду PostgreSQL с помощью объекта NetworkPolicy.