PostgreSQL 安全最佳實踐

作者 | Roman Tkachenko

譯者 | 馬可薇

策劃 | 萬佳

數據庫是黑客眼中的 “聖盃”,需要我們像對待“花朵” 一樣精心呵護。本文主要介紹數據庫保護的最佳實踐。

首先,從最常用的開源數據庫 PostgreSQL 開始,我們將一一介紹諸位需要考慮的幾個安全層級:

1PostgreSQL 中網絡層的安全

理想情況下,PostgreSQL 服務器應當是完全隔離,不允許任何入站申請、SSH 或 psql 的。然而,PostgreSQL 沒有對這類網閘設置提供開箱即用的支持。

我們最多也只能通過設置防火牆,鎖定數據庫所在節點的端口級訪問來提升數據庫服務器的安全性。默認情況下,PostgreSQL 監聽 TCP 端口 5432。而根據操作系統的不同,鎖定其他端口的方式也會有所不同。以 Linux 最常用的防火牆iptables爲例,下面這幾行代碼就可輕鬆完成任務:

\# 確保已有連接不被 drop
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
# 允許 SSH.
iptables -A INPUT -p tcp -m state --state NEW --dport 22 -j ACCEPT
# 允許 PostgreSQL.
iptables -A INPUT -p tcp -m state --state NEW --dport 5432 -j ACCEPT
# 允許所有出站,drop 所有入站
iptables -A OUTPUT -j ACCEPT
iptables -A INPUT -j DROP
iptables -A FORWARD -j DROP

在更新 iptables 的規則時,建議使用 iptables-apply 工具。這樣,哪怕你不小心把自己也鎖在外面了,它也能自動回滾更改。

這條 PostgreSQL 規則會允許所有人連接到端口 5432 上。當然,你也可以將其修改爲只接受特定 IP 地址或子網讓限制更加嚴格:

\# 僅允許本地子網訪問 PostgreSQL 端口
iptables -A INPUT -p tcp -m state --state NEW --dport 5432 -s 192.168.1.0/24 -j ACCEPT

繼續討論我們的理想情況。想要完全限制到 5432 端口的所有入站連接需要一個某種類型的本地代理,由它維持一個到客戶端節點的持久出站連接,並且能將流量代理到本地 PostgreSQL 實例。

這種代理被稱作是 “反向通道”。具體使用方法可以通過 SSH 遠程端口轉發的功能進行演示。運行下列指令可以在 PostgreSQL 數據庫運行的節點上打開一個反向通道:

ssh -f -N -T -R 5432:localhost:5432 user@<client-host>

PostgreSQL 的節點需要能訪問,其上的 SSH 守護進程(daemon)也要處於運行狀態。下列指令會將數據庫服務器上端口 5432 轉發到客戶端機器的端口 5432 上,這樣你就可以通過這個通道連接數據庫了:

psql "host=localhost port=5432 user=postgres db

PostgreSQL 監聽地址

通過配置文件指令listen_addresses來限制服務器監聽客戶端連接的地址是個好習慣。如果運行 PostgreSQL 的節點上有多個網絡接口,listen_addresses可以確保服務器只會監聽客戶端所連接的一個或多個接口:

如果連接到數據庫的客戶端總是駐留在同一節點上,或是與數據庫共同駐留在同一個 Kubernetes pod 上,PostgreSQL 作爲 sidecar 容器運行,禁用套接字監聽(socket)可以完全消除網絡的影響。將監聽地址設置爲空字符串可以使服務器只接受 Unix 域的套接字連接:

listen\_addresses = ''

2PostgreSQL 中傳輸層級的安全

當世界上大部分的網絡都轉投向 HTTP 的懷抱時,爲數據庫連接選擇強傳輸加密也變成了一個必備項目。PostgreSQL 本身即支持 TLS(因爲歷史遺留問題,在文檔、配置文件,以及 CLI 中仍被稱作是 SSL),我們也可以使用它進行服務器端和客戶端認證。

服務器端 TLS

對於服務器的認證,我們首先需要爲服務器準備一份用於和相連接的客戶端認證的證書。在 Let's Encrypt 上,我們可以找到免費提供的 X.509 證書,具體使用方法以 certbot 的命令行工具爲例:

certbot certonly --standalone -d postgres.example.com

需要注意的是,certbot 默認使用 ACME 規範的 HTTP-01 挑戰來驗證證書請求,這裏我們就需要確保請求域指向節點的 DNS 有效,並且端口 80 處於開放狀態。

除了 Let's Encrypt 之外,如果想在本地生成所有的信息,我們還可以選擇 openssl 命令行工具:

\# 生成一個自簽名的服務器 CA
openssl req -sha256 -new -x509 -days 365 -nodes \\
    -out server-ca.crt \\
    -keyout server-ca.key
# 生成服務器 CSR,CN 裏填需要連接到數據庫的主機名 
openssl req -sha256 -new -nodes \\
    -subj "/CN=postgres.example.com" \\
    -out server.csr \\
    -keyout server.key
# 簽證書
openssl x509 -req -sha256 -days 365 \\
    -in server.csr \\
    -CA server-ca.crt \\
    -CAkey server-ca.key \\
    -CAcreateserial \\
    -out server.crt

在生產環境中記得在證書過期前更新。

客戶端 TLS

通過驗證客戶端提供的 X.509 證書是由可信的證書頒發機構(CA)簽名,服務器便可以驗證連接客戶端的身份。

\# 生成一個自簽名的客戶端 CA
openssl req -sha256 -new -x509 -days 365 -nodes \\
    -out client-ca.crt \\
    -keyout client-ca.key
# 生成客戶端 CSR。CN 必須填用於連接的數據庫角色名
openssl req -sha256 -new -nodes \\
    -subj "/CN=alice" \\
    -out client.csr \\
    -keyout server.key
# 簽證書
openssl x509 -req -sha256 -days 365 \\
    -in client.csr \\
    -CA client-ca.crt \\
    -CAkey client-ca.key \\
    -CAcreateserial \\
    -out client.crt

TLS 配置

小結一下,我們現在可以配置 PostgreSQL 服務器來接收 TLS 連接了:

ssl = on
ssl\_cert\_file = '/path/to/server.crt'
ssl\_key\_file = '/path/to/server.key'
ssl\_ca\_file = '/path/to/client-ca.crt'
# 這裏默認是on,但出於安全考慮,特意寫出來總是沒錯的 
ssl\_prefer\_server\_ciphers = on
# TLS 1.3能提供最強的安全保護。在控制服務器和客戶端時建議使用ssl\_min\_protocol\_version = 'TLSv1.3'

我們還需要再配置的就只剩下用於更新 PostgreSQL 服務器的基於主機的認證文件(pg_hba.conf)了。它可以要求所有的連接使用 TLS,並通過 X.509 證書對客戶端進行認證。

\# TYPE  DATABASE        USER            ADDRESS                 METHOD
hostssl all             all             ::/0                    cert
hostssl all             all             0.0.0.0/0               cert

現在,連接到數據庫服務器的客戶端就需要提供由客戶端 CA 簽名的有效證書了:

psql "host=postgres.example.com \\
      user=alice \\
      dbname=postgres \\
      sslmode=verify-full \\
      sslrootcert=/path/to/server-ca.crt \\
      sslcert=/path/to/client.crt \\
      sslkey=/path/to/client.key"

需要注意的是,默認情況下,psql 不會進行服務器證書認證,所以我們需要將 “sslmode” 設置爲“verify-full” 或者 “verify-ca”。具體設置要看你是否使用了編碼後的 X.509 中 CN 字段的主機名對服務器進行連接。

爲了簡化指令,並且避免在每次連接到數據庫時都要重新輸入一遍到 TLS 的路徑,可以通過使用 PostgreSQL 的連接服務文件來解決。它可以將連接參數分組到 “services” 中,通過連接字符串中的 “service” 參數進行引用。

用下列代碼創建 “~/.pg_service.conf” 文件:

\[example\]
host=postgres.example.com
user=alice
sslmode=verify-full
sslrootcert=/path/to/server-ca.crt
sslcert=/path/to/client.crt
sslkey=/path/to/client.key

現在,當連接到數據庫時,我們只需要指定服務名和想要連接的數據庫名即可:

psql "service=example db

3PostgreSQL 中數據庫層級安全

角色

目前爲止,我們已經探討的內容有以下幾點:如何從未認證的網絡連接中保護 PostgreSQL 數據庫,如何使用強加密進行數據傳輸,以及如何通過共同的 TLS 認證讓服務器與客戶互相信任對方身份。接下來,我們將繼續分析用戶在這裏能做什麼,連接到數據庫後他們可以訪問什麼,以及如何驗證他們的身份。這一步通常被稱作是 “授權”。

PostgreSQL 有一套完整的、依據角色(role)建立的用戶權限系統。在現代的 PostgreSQL(8.1 及以上版本)中,“角色”是 “用戶” 的同義詞。無論你用什麼樣的數據庫賬戶名,比如 psql 中的 "user=alice",它其實都是一個可以連接數據庫的、擁有 LOGIN 屬性的角色。也就是說,下面兩條指令其實效果一樣:

CREATE USER alice;
CREATE ROLE alice LOGIN;

除了登入的權限外,角色還可以擁有其他屬性:可以通過所有的權限檢查的SUPERUSER,可以創建數據庫的CREATEDB,可以創建其他角色的CREATEROLE,等等。

除了屬性外,角色被授予的權限可以分爲兩大類:一是其他角色的成員身份(membership),二是數據庫對象的權限。下面我們將介紹這兩類都是如何工作的。

授予角色權限

假設,我們需要跟蹤服務器清單:

CREATE TABLE server\_inventory (
    id            int PRIMARY KEY,
    description   text,
    ip\_address    text,
    environment   text,
    owner         text,
);

默認情況下,PostgreSQL 安裝會包含一個用於引導數據庫的超級用戶角色,通常被稱作是 “postgres”。使用這一角色進行所有的數據庫操作相當於在 Linux 系統中經常使用“root” 登入,永遠不是個好主意。所以,我們要創建一個無權限角色,根據需要爲其授予最小權限。

通過創建一個 “組角色” 並授權其他角色(角色與用戶一一對應)爲組內成員,可以避免爲每一個用戶或角色單獨分配權限的麻煩。假如說,我們想要授予開發者 Alice 和 Bob 查看服務器清單,但不允許其修改的權限:

\-- 創建一個自身沒有登入能力的組角色,授予其在服務器清單表中進行 SELECT 的權限
GRANT SELECT ON server\_inventory TO developer;
-- 創建兩個用戶賬號,在登入權限的基礎上繼承“developer”權限 
CREATE ROLE alice LOGIN INHERIT;
CREATE ROLE bob LOGIN INHERIT;
-- 分配給兩個用戶賬號“developer”組角色
GRANT developer TO alice, bob;

現在,當連接到數據庫時,Alice 和 Bob 都會繼承 “developer” 組角色中的權限,並且可以查詢數據庫清單表。

SELECT權限默認對錶的所有列有效,但這一點也是可以更改的。假設我們只想讓實習生查看服務器的大致信息但又不想讓他們連接到服務器,那麼就可以選擇隱藏 IP 地址:

CREATE ROLE intern;
GRANT SELECT(id, description) ON server\_inventory TO intern;
CREATE ROLE charlie LOGIN INHERIT;
GRANT intern TO charlie;

https://www.postgresql.org/docs/13/ddl-priv.html?fileGuid=jWHTCry9KtVKqy3D

行級安全策略

PostgreSQL 權限系統的更高級的玩法是行級安全策略(RLS),它允許你爲表中的部分行分配權限。這既包括可以用SELECT查詢的行,也包括可以INSERTUPDATE以及DELETE的行。

想要使用行級安全,我們需要準備兩件事情:在表中啓用 RLS,爲其定義一個用於控制行級訪問的策略。

繼續之前的例子,假設我們只想允許用戶更新他們自己的服務器。那麼,第一步,啓用表中的 RLS:

ALTER TABLE server\_inventory ENABLE ROW LEVEL SECURITY;

如果沒有定義任何策略的話,PostgreSQL 會默認到 “拒絕” 策略上,這就意味着除了表的創建者 / 所有者之外,沒有任何角色能夠訪問。

行安全策略是一個布爾表達式,是 PostgreSQL 用於判定所有需要返回或更新的行的條件。SELECT 語句返回的行將對照 USING 子語句所指定的表達式進行檢查,而通過 INSERT,UPDATE 或 DELETE 語句更新的行將對照 WITH CHECK 表達式進行檢查。

首先,讓我們定義幾個策略,允許用戶查看所有服務器,但只能更新他們自己服務器,這個 “自己” 是由表中的 “owner” 字段決定的。

CREATE POLICY select\_all\_servers
    ON server\_inventory FOR SELECT
    USING (true);
CREATE POLICY update\_own\_servers
    ON server\_inventory FOR UPDATE
    USING (current\_user = owner)
    WITH CHECK (current\_user = owner);

注,只有表的所有者可以創建或更新 RLS。

審計

到目前爲止,我們主要都在討論先手防禦的安全措施。根據其中一項基本的安全原則——深度防禦,我們分析了這些措施是如何通過互相疊加,幫助減緩(假想中的)攻擊者進攻系統的進程。

留存準確且詳細的審計追蹤記錄是系統安全屬性中常常被忽視的一點。監控數據庫服務器端的網絡層或節點層訪問並不在本文的討論範圍內,但當涉及到 PostgreSQL 服務器本身時,不妨先來看看我們有什麼可選項。

想要更清晰地觀測數據庫內情況時,最常用的方法是啓用詳細日誌記錄。在服務器配置文件中添加以下指令,開啓對所有連接嘗試以及已執行 SQL 的日誌記錄。

; 記錄成功與非成功連接嘗試 
log\_connections = on
; 記錄終止對話
log\_disconnections = on
; 記錄所有執行過的 SQL 語句
log\_statement = all

不幸的是,對於標準的自託管 PostgreSQL,這幾乎是在不安裝其他插件的情況下你能做到的全部了。雖然總比沒有強,但在少數數據庫服務器和簡單的 “grep” 之外,它的延展性並不好。

如果想要更高級的 PostgreSQL 審計解決方案,諸如 pgAudit 這樣的第三方插件是個不錯的選擇。自託管的 PostgreSQL 需要手動安裝,但對於一些託管版本的 PostgreSQL,比如 AWS RDS 這些,本身就有,我們只需啓用即可。

對於記錄的語句,pgAudit 提供了更多的結構和粒度。但要記住,它並沒有脫離日誌的範圍,也就是說,如果需要將審計日誌以結構化的格式傳輸到外部 SIEM 系統以作更詳細的分析時,恐怕會很難。

4PostgreSQL 中基於證書的訪問

Teleport 數據庫訪問(Teleport for Database Access)是一款開源項目,在它的幫助下,我們可以實現本文中介紹的所有用於保護 PostgreSQL 和其他數據庫的最佳實踐。

用戶可以通過單點登錄流程訪問數據庫,使用短期 X.509 證書代替常規憑證進行認證。

數據庫不需暴露在公網中,可以使用 Teleport 內置的反向通道子系統在網閘環境中安全運行。

管理員和審計人員可以在審計日誌中查看數據庫活動,例如與特定用戶標識相關聯的會話和 SQL 語句,並可以選擇將其傳送至外部系統。

5 結語

與任何以安全爲前提設計的系統一樣,正確地保護對數據庫實例的訪問需要在網絡協議棧的多個層次上採取保護措施。在這篇文章中,我們從網絡和傳輸安全開始,探討了如何使用 PostgreSQL 靈活的用戶權限系統。

原文鏈接:

https://goteleport.com/blog/securing-postgres-postgresql?fileGuid=jWHTCry9KtVKqy3D

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。