Postgres - rotate password with zero downtime

為了資安安全性的問題,定期的 rotate password 是該做的事情,但問題是:rotate password 要如何避免 application 有 downtime?

以下紀錄 PostgreSQL 如何快速 rotate password with zero downtime。

如何快速在 Postgres Rotate Password

假設 application 在用的 role name 是 app,則整體 rotate password 策略如下:

  1. 建立 temp role 叫做 app_temp 並且繼承 app role 的所有權限,密碼也可以設定跟 app role 一樣
  2. 設定 app_temp role 連線後自動 SET ROLE 為 app,這樣就等同用 app role 來做任何操作
  3. 將 application rolling update 改用 app_temp role 對 postgres 進行連線
  4. 確認 app role 已經沒有任何 application 在使用,則將 app role 更新密碼
  5. 將 application rolling update 改回用原本的 app role 對 postgres 進行連線
  6. app_temp role DROP 掉

說是完全 zero downtime 可能不是那麼精確,但如果你的 application 有做好 graceful shutdown 並且有 rolling update 通常 client 端是不會注意到 downtime 的。至少比直接改現有 role 的 password 並強制踢掉 client 連線並重連會是更好的體驗。

以下給個 psql example 示範:

  1. 建立 app role

    1
    2
    postgres=# CREATE ROLE app WITH LOGIN PASSWORD 'oldpassword';
    CREATE ROLE
  2. app role 來建立測試 table 並 insert data

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    psql -U app -d postgres -W -h 127.0.0.1
    Password:
    postgres=> select current_user;
    current_user
    --------------
    app
    (1 row)

    postgres=> CREATE TABLE tests(id INTEGER PRIMARY KEY);
    CREATE TABLE
    postgres=> INSERT INTO tests(id) VALUES(1);
    INSERT 0 1
    postgres=> SELECT * FROM tests;
    id
    ----
    1
    (1 row)

    這時候可以 SELECT tests table owner 確認一下:

    1
    2
    3
    4
    postgres=> select * from pg_tables where tablename = 'tests';
    schemaname | tablename | tableowner | tablespace | hasindexes | hasrules | hastriggers | rowsecurity
    ------------+-----------+------------+------------+------------+----------+-------------+-------------
    public | tests | app | | t | f | f | f
  3. 建立 app_temp role 並繼承 app role 的權限

    1
    2
    3
    4
    postgres=> CREATE ROLE app_temp WITH LOGIN PASSWORD 'oldpassword' IN ROLE app;
    CREATE ROLE
    postgres=> ALTER ROLE app_temp SET ROLE app;
    ALTER ROLE

    這時候 app_temp 再重新登入就會發現 current_user 為 app

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    psql -U app_temp -d postgres -W -h 127.0.0.1
    Password:
    psql (16rc1, server 11.21)
    Type "help" for help.

    postgres=> select current_user;
    current_user
    --------------
    app
    (1 row)

    當然這時候去 SELECT tests table 是可以看得到的:

    1
    2
    3
    4
    5
    postgres=> SELECT * FROM tests;
    id
    ----
    1
    (1 row)

    這時候你可能會好奇,為什麼還要 SET ROLE 因為 app_temp 本身就有 app 的權限了呀。原因在於,你用 app_temp role 做的任何操作都是用 app_temp 的身份,也就是說如果你 CREATE 新 TABLE,這個 table owner 是掛在 app_temp 上的。

    而通常換 role name 反而是很不方便的,你這樣可能其他有關聯到的服務都要跟著改設定檔,這就是為啥之後還需要將 app role 還原回去給 application 做使用,因此才需要做 SET ROLE。

    之後你就可以對 application 做 rolling update,而且因為密碼與之前相同,DB connection 只需要改 username 即可。

  4. app role 進行更換密碼

    1
    2
    3
    4
    postgres=# \password app
    Enter new password for user "app":
    Enter it again:
    postgres=#

    我建議不要用 ALTER ROLE app SET PASSWORD 'xxxx' 這樣的做法,原因是這樣會留下 psql history 或是任何 log 機制都可能會看到 plain password。當然你是可以 SET PASSWORD 給 hash 的結果,PG 會根據你的 hash 去判斷你給的 password 是用 MD5 還是 SHA256,但這樣也是滿麻煩的。

    因此建議用 \password 的方式來進行密碼更動。

    更換完之後,就可以一樣對 application 做 rolling update,DB connection 要改 username 跟 password。

總結

雖然這樣 rotate 是比較麻煩的,但是用 K8S 這類的工具幫你做 rolling update 且 application 有做好 graceful shutdown 的話,這樣已經盡可能降低 downtime 了,基本上使用者應該是根本不會發現才對。

題外話,有翻到這篇 Rotate database credentials without restarting containers,我大概看了一下做法就是在 client 端會定期去跟 secret manager 之類的服務拿憑證去連線資料庫,當過期的時候 client 端的 code 會斷掉連線拿最新的憑證進行重連。這樣的確就不需要 restart,不過還是需要 disconnect 在重連,只是速度更快也是一種方式,但就是要在 client 端的 code 做手腳。