2021年8月13日 星期五

EDB 與 PGSQL 密碼管控比較

資料庫在管理上會需要進行密碼的管控,以及密碼複雜度等設定,在 ORACLE 部分有 PASSWORD PROFILE 的功能可以使用,而在 EDB 企業版 PGSQL 中有對應相容的 PASSWORD PROFILE 能夠使用,這邊會針對 EDB 以及 PGSQL 在相關密碼管控上的功能進行比較與討論。

1. EDB 支援 ORACLE 相容性的 PASSWORD PROFILE 功能,能夠自定義密碼複雜度檢查函數(包含與前次密碼進行檢查)密碼重複使用次數密碼過期時間,相關的檢查僅會在更改密碼時生效,不會影響當前使用者,但是在 PROFILE 部分如果有設定過期時間會生效開始計時。

2.PGSQL 可以透過啟用 passwordcheck 模組來進行密碼複雜度的檢查,只需要在設定檔案 potsgresql.auto.conf 裡面新增 shared_preload_libraries = '$libdir/passwordcheck' 後重啟即可生效,預設檢查密碼長度至少八個字必須包含英數字不能與使用者名稱相同,此檢查會涵蓋所有使用者,可以透過手動改寫C程式碼重新編譯的方式來做更複雜的調整。

另外在編譯過程可選擇啟用 CrackLib 的 Library 支援來阻擋容易被破解的密碼,並且可以自定義檢查用的字典。

目前 PGSQL 無法進行檢查密碼是否重複使用,也無法與前一個密碼進行比較是否有足夠差異。

3. PGSQL 可以透過設定 password valid time 來限制密碼使用期限,此設定與 PROFILE 的功能不相同,比較像是限制帳號的使用期限,因此不會隨著密碼更新而刷新時間,並且僅能透過管理者去移除設定與調整時間,由於管理員除非有紀錄 pg_shadow 裡面加密過後的密碼(不建議),不然無法比較確認使用者是否有更改過密碼進而去調整過期時間,並不適合用來限制密碼過期使用

4. 不論是 EDB 的 password profile 或是 PGSQL 使用 passwordcheck 都僅能對明碼進行密碼複雜度的檢查,而如果使用加密過的密碼(md5XXXXX),做修改的時候則僅會檢查是否與帳號名稱一致,而不會做其餘檢查,因此如果使用到 psql 功能 \password 的時候,輸入的密碼會自動被轉碼成加密過的格式,因此僅會檢查密碼使否與使用者名稱相同,或是是否有重複使用,並無法檢查密碼檢查函數裡面設定的相關規則。

5. 如果在修改時欲使用加密過的密碼可以使用以下指令,將結果加上 md5 後進行修改密碼,或是用 psql 功能 \password。

[enterprisedb@ ~]$ echo -n "password+username" | md5sum
edb=# alter user username password 'md5714c22dab12a8211a0915aa4b343b0c1';


下面會針對 PGSQL 的 passwordcheck 模組做簡單的改寫範例,此處不針對 EDB 版本做測試,在 EDB 直接使用 PROFILE 功能即足夠應付密碼安全性需求。

本處需要下載原始碼進行編譯

wget postgresql-13.4.tar.gz  
tar -zxvf postgresql-13.4.tar.gz  
cd postgresql-13.4/contrib/passwordcheck/
cp passwordcheck.c passwordcheck.c.bkp # 先備份檔案方便後面重試
vi passwordcheck.c

主要編輯 check_password 這個函數內容,來修改密碼複雜度檢查規範,此處新增一範例檢查密碼是否為帳號倒過來,以及調整對於英數字數量的長度限制。

#define MIN_PWD_LENGTH 8  /*可以修改此處調整密碼最低長度*/

check_password(const char *username,
                           const char *shadow_pass,
                           PasswordType password_type,
                           Datum validuntil_time,
                           bool validuntil_null)
{
        if (prev_check_password_hook) /*加密過後的密碼走此處檢查*/
                prev_check_password_hook(username, shadow_pass,
                                                                 password_type, validuntil_time,
                                                                 validuntil_null);

        if (password_type != PASSWORD_TYPE_PLAINTEXT) /*一般使用沒加密過後的密碼走此處檢查*/
        {
                /*
                 * Unfortunately we cannot perform exhaustive checks on encrypted
                 * passwords - we are restricted to guessing. (Alternatively, we could
                 * insist on the password being presented non-encrypted, but that has
                 * its own security disadvantages.)
                 *
                 * We only check for username = password.
                 */
                char       *logdetail;

                if (plain_crypt_verify(username, shadow_pass, username, &logdetail) == STATUS_OK)
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password must not equal user name")));
            }
            else
            {
                /*
                 * For unencrypted passwords we can perform better checks
                 */
                const char *password = shadow_pass;
                int                     pwdlen = strlen(password);
                int                     i;
                int             pwd_has_letter,
                                                pwd_has_nonletter;
                int             pwd_is_reverse;

                /* 檢查密碼最小長度 enforce minimum length */
                if (pwdlen < MIN_PWD_LENGTH)
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password is too short")));

                /* check if the password contains the username */
                if (strstr(password, username))
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password must not contain user name")));

                /* check if the password is the reverse of username 檢查密碼是否為帳號倒過來*/
                if (strlen(username) == pwdlen)
                {
                     pwd_is_reverse = 0;
                     for ( i = 0; i < pwdlen; i++ )
                     {
                            if ( password[pwdlen-i-1] == username[i])
                                     pwd_is_reverse++;
                     }

                     if (pwd_is_reverse == pwdlen)
                            ereport(ERROR,
                                          (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password must not be the reverse of user name")));
                }

                /* check if the password contains both letters and non-letters */
                pwd_has_letter = 0;
                pwd_has_nonletter = 0;
                for (i = 0; i < pwdlen; i++)
                {
                        /*
                         * isalpha() does not work for multibyte encodings but let's
                         * consider non-ASCII characters non-letters
                         * 此處調整改為檢查英文字母和非英文字母都必須至少包含三個以上
                         */
                        if (isalpha((unsigned char) password[i]))
                                pwd_has_letter++;
                        else
                                pwd_has_nonletter++;
                }
                if (pwd_has_letter < 3 || pwd_has_nonletter < 3)
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password must contain both 3 letters and 3 nonletters")));

#ifdef USE_CRACKLIB
                /* call cracklib to check password */
                if (FascistCheck(password, CRACKLIB_DICTPATH))
                        ereport(ERROR,
                                        (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
                                         errmsg("password is easily cracked")));
#endif
        }

        /* all checks passed, password is ok */
}

修改完成後初次執行需要進行完整編譯。
cd /root/postgresql-13.4
./configure
gmake world
如果已經進行過完整編譯,後續在測試的時候可以進行部分的編譯即可
cd /root/postgresql-13.3/contrib/passwordcheck/
make clean;
make;

 完成後將檔案複製到當前資料庫 LIB 目錄後啟動資料庫進行測試使用,如果有狀況則回頭修改 passwordcheck.c 檔案後再重新編譯安裝。

cp /usr/pgsql-13/lib/passwordcheck.so ~/passwordcheck.so.bkp
cp /root/postgresql-13.3/contrib/passwordcheck/passwordcheck.so /usr/pgsql-13/lib/
service postgresql-13 start

測試使用

-bash-4.2$ psql -E
psql (13.3)
Type "help" for help.

postgres=# create user abcd1234;
CREATE ROLE
postgres=# alter user abcd1234 password 'abcd';
ERROR:  password is too short
postgres=# alter user abcd1234 password 'abcd1234';
ERROR:  password must not contain user name
postgres=# alter user abcd1234 password 'abcdefghi12';
ERROR:  password must contain both 4 letters and 4 nonletters
postgres=# alter user abcd1234 password '4321dcba';
ERROR:  password must not be the reverse of user name
postgres=# \password    -- 此處加密因此不受密碼檢查影響
Enter new password:abc
Enter it again:abc
********* QUERY **********
ALTER USER postgres PASSWORD 'SCRAM-SHA-256$4096:TEzknK706ouVnIb+gXDmeg==$U7QDORNSX3Dkmo5y5s+PeggplFYjKk/iAb8z/diA8WY=:6OUaunD7wtxHfV+gZjnb/Ycvc32ME2cgNWlXcMRDSbs='
**************************
postgres=#

透過以上方法進行改寫,可以依需求自定義更複雜的密碼檢查函數,至於 CrackLib 字典的部分可以參考連結有相關的示範這邊不額外做測試範例,目前測試使用上,PGSQL 相對 EDB 主要兩個不足的地方在於無法設置密碼過期時間、無法檢查密碼是否重複使用,另外密碼規範只能做全域的規範,無法針對使用者進行各別設定,除非在密碼檢查函數裡面透過 username 做更複雜的改寫。

參考連結:

https://www.modb.pro/db/32449?ywm

https://www.postgresql.org/docs/13/passwordcheck.html

https://paquier.xyz/postgresql-2/postgres-module-highlight-customize-passwordcheck-to-secure-your-database/

https://billtian.github.io/digoal.blog/2014/10/09/01.html

https://ravenonhill.blogspot.com/2017/03/postgresql-edb-password-profile.html