安全與容器限制策略:容器化應用的防護思考

在假設 identidock 服務具有一定價值並擁有使用者基礎的前提下,我們需要建立完善的安全防護措施。當使用者依賴此服務時,確保其持續可靠運作變得尤為重要。以下是我在實際佈署中會優先考慮的核心安全策略:

基礎安全措施實施清單

首先,我會確保這些基本安全措施到位:

  • 環境隔離:將 identidock 容器執行在獨立虛擬機器或專用主機上,防止其他服務遭受潛在攻擊的連帶影響
  • 最小化暴露面:僅允許負載平衡器/反向代理容器對外開放連線埠,大幅減少攻擊面,同時確保監控或日誌服務僅透過私有介面或 虛擬私人網路 存取
  • 非 root 使用者執行:為所有 identidock 映像檔定義非特權使用者,避免以 root 身份執行
  • 映像檔來源驗證:透過雜湊值下載或其他安全驗證方式取得所有映像檔
  • 監控與警示系統:佈署監控系統以偵測異常流量或行為模式
  • 軟體更新與保護模式:確保所有容器執行最新軟體與處於生產模式,關閉所有除錯資訊
  • 強化主機安全:在主機上啟用 AppArmor 或 SELinux(如可用)
  • 加強 Redis 存取控制:為 Redis 新增某種形式的存取控制或密碼保護

進階安全強化措施

在有充足時間的情況下,我會進一步實施這些強化措施:

  • 移除不必要的 setuid 二進位檔:從映像檔中移除不必要的 setuid 二進位檔,降低攻擊者提升許可權的風險
  • 唯讀檔案系統:盡可能以唯讀方式執行檔案系統(dnmonster、identidock 和 redis 容器可以使用唯讀容器檔案系統,但 redis 掛載卷必須可寫入)
  • 限制核心許可權:讓 dnmonster 和 identidock 容器在丟棄所有功能的情況下執行

高安全需求場景的特殊措施

對於安全敏感度更高的服務,我會考慮這些更嚴格的措施:

  • 記憶體限制:使用 -m 引數限制每個容器的記憶體使用,防止某些 DoS 攻擊和記憶體洩漏
  • SELinux 專用類別:為容器執行帶有專用類別的 SELinux,雖然設定工作量大,但在使用 devicemapper 儲存驅動時非常有效
  • 程式數量限制:對程式數量設定 ulimit,防止 fork bomb DoS 攻擊
  • 內部通訊加密:加密內部通訊,增加攻擊者篡改資料的難度

即使是像 identidock 這樣的小型應用,也需要實施多層次的安全措施,並定期審核系統確保一切正常運作與沒有容器佔用過多資源。

核心原則是:建立越多的檢查和邊界,就越能在攻擊造成實質傷害前阻止它。不正確使用 Docker 會降低系統安全性,開啟新的攻擊向量;而正確使用則能透過增加隔離層級和限制應用程式對系統的影響範圍,提高整體安全性。

按主機隔離容器

在多租戶環境中(無論是組織內部使用者還是外部客戶),我建議將每個使用者的容器放在獨立的 Docker 主機上,如圖 13-1 所示。雖然這種做法比在使用者間分享主機效率低,會導致更多的虛擬機器或實體機器,但對安全性至關重要。

主要原因是防止容器突破隔離後,攻擊者取得其他使用者的容器或資料存取權。若發生容器突破,攻擊者仍被限制在獨立的虛擬機器或機器上,無法輕易存取屬於其他使用者的容器。

同樣,處理或儲存敏感資訊的容器應與處理較不敏感資訊的容器分開,特別是要遠離直接暴露給終端使用者的應用程式容器。例如,處理信用卡詳細資料的容器應與執行 Node.js 前端的容器分開佈署。

隔離和使用虛擬機器還能提供對抗 DoS 攻擊的額外保護;如果使用者被限制在自己的虛擬機器中,他們就無法壟斷主機記憶體並導致其他使用者的服務資源匱乏。

在短期到中期內,絕大多數容器佈署將涉及虛擬機器。雖然這不是理想情況,但它確實意味著我們可以結合容器的效率和虛擬機器的安全性。

應用更新策略

快速對執行系統應用更新對維護安全性至關重要,尤其是當常用工具和框架出現漏洞時。

容器化系統的更新過程大致包含以下步驟:

  1. 識別需要更新的映像檔:包括基礎映像檔和所有依賴映像檔(可使用 CLI 取得執行容器的映像檔列表)
  2. 取得或建立基礎映像檔的更新版本:將此版本推播到你的登入檔或下載站點
  3. 重建依賴映像檔:對每個依賴映像檔,使用 --no-cache 引數執行 docker build,並推播這些映像檔
  4. 更新主機映像檔:在每個 Docker 主機上執行 docker pull 確保取得最新映像檔
  5. 重啟容器:在每個 Docker 主機上重啟容器
  6. 清理舊映像檔:確認一切正常運作後,從主機移除舊映像檔,如果可能,也從登入檔中移除

有些步驟實際操作起來並不簡單。識別需要更新的映像檔可能需要一些指令碼工作;重啟容器則假設你已有某種滾動更新支援或能容忍一定的停機時間。在撰寫本文時,完全從登入檔移除映像檔並回收磁碟空間的功能仍在開發中。

如果你使用 Docker Hub 構建映像檔,可以設定儲存函式庫,當任何連結的映像檔變更時,會自動觸發你的映像檔重建。透過設定與基礎映像檔的連結,當基礎映像檔變更時,你的映像檔將自動重建。

取得執行映像檔列表

以下命令取得所有執行容器的映像檔 ID:

$ docker inspect -f "{{.Image}}" $(docker ps -q)
42a3cf88f3f0cce2b4bfb2ed714eec5ee937525b4c7e0a0f70daff18c3f2ee92
41b730702607edf9b07c6098f0b704ff59c5d4361245e468c0d551f50eae6f84

我們可以使用更多 shell 技巧取得更多資訊:

$ docker images --no-trunc | \
grep $(docker inspect -f "-e {{.Image}}" $(docker ps -q))
nginx latest 42a3cf88f... 2 weeks ago 132.8 MB
debian latest 41b730702... 2 weeks ago 125.1 MB

要取得所有映像檔及其基礎或中間映像檔的列表(使用 --no-trunc 取得完整 ID):

$ docker inspect -f "{{.Image}}" $(docker ps -q) | \
xargs -L 1 docker history -q
41b730702607
3cb35ae859e7
42a3cf88f3f0
e59ba510498b
50c46b6286b9
ee8776c93fde
439e7909f795
0b5e8be9b692
e7e840eed70b
7ed37354d38d
55516e2f2530
97d05af69c46
41b730702607
3cb35ae859e7

進一步擴充套件以取得映像檔資訊:

$ docker images | \
grep $(docker inspect -f "{{.Image}}" $(docker ps -q) | \
xargs -L 1 docker history -q | sed "s/^/\-e /")
nginx latest 42a3cf88f3f0 2 weeks ago 132.8 MB
debian latest 41b730702607 2 weeks ago 125.1 MB

如果要取得中間映像檔以及命名映像檔的詳細資訊,可以在 docker images 命令中新增 -a 引數。

這個命令有一個重要陷阱:如果你的主機沒有一個基礎映像檔的標籤版本,它將不會出現在列表中。例如,官方 Redis 映像檔根據 debian:wheezy,但除非主機單獨與明確提取了 debian:wheezy 映像檔(與是完全相同的版本),否則基礎映像檔將在 docker images -a 中顯示為 <None>

當需要修補第三方映像檔(包括官方映像檔)中發現的漏洞時,你依賴該方提供及時更新。在過去,提供者因反應緩慢而受到批評。在這種情況下,你可以等待或準備自己的映像檔。假設你有映像檔的 Dockerfile 和來源,自行構建映像檔可能是一個簡單有效的臨時解決方案。

這種方法與典型的虛擬機器方法形成對比,後者使用 Puppet、Chef 或 Ansible 等設定管理軟體。在設定管理方法中,虛擬機器不會被重新建立,而是根據需要透過 SSH 命令或在虛擬機器中安裝的代理進行更新和修補。這種方法有效,但意味著不同的虛擬機器通常處於不同的狀態,並涉及大量複雜性來追蹤和更新虛擬機器。這種複雜性是必要的,以避免重新建立虛擬機器和維護服務的主映像檔或黃金映像檔的開銷。

容器也可以採用設定管理方法,但這會增加不必要的複雜性——由於容器啟動速度快、構建和維護映像檔容易,簡單的黃金映像檔方法與容器配合良好。這與現代不可變基礎設施的理念相似,其中基礎設施(包括裸機、虛擬機器和容器)從不修改,而是在需要變更時替換。

標籤你的映像檔

透過在構建映像檔時大量使用標籤,可以更容易地識別映像檔及其內容。這個功能出現在 1.6 版本中,允許映像檔建立者將任意鍵值對與映像檔關聯。這可以在 Dockerfile 中完成:

FROM debian
LABEL version 1.0
LABEL description "A test image for describing labels"

你可以進一步新增資料,如映像檔中程式碼編譯自的 git 雜湊值,但這需要使用某種範本工具自動更新值。

標籤也可以在執行時新增到容器:

$ docker run -d --name label-test \
-l group=a debian sleep 100
1d8d8b622ec86068dfa5cf251cbaca7540b7eaa67664a13c620006...
$ docker inspect -f '{{json .Config.Labels}}' label-test
{"group":"a"}

當你想處理某些執行時事件(如動態將容器分配到負載平衡器組)時,這非常有用。

有時你需要更新 Docker 守護程式以取得新功能、安全補丁或錯誤修復。這將迫使你要麼將所有容器遷移到新主機,要麼在應用更新時暫時停止它們。建議你訂閱 docker-user 或 docker-dev Google 群組,以接收重要更新的通知。

避免不受支援的驅動程式

儘管 Docker 年輕,但它已經歷了多個發展階段,各種功能已被棄用或不再維護。依賴此類別功能存在安全風險,因為它們不會像 Docker 其他部分那樣受到同樣的關注和更新。同樣的情況也適用於 Docker 依賴的驅動程式和擴充套件。

特別是,不要使用舊版 LXC 執行驅動程式。預設情況下,這是關閉的,但你應該檢查你的守護程式是否使用了 -e lxc 引數執行。

在容器安全領域工作多年,玄貓發現多層防禦策略始終是最有效的。不要依賴單一安全機制,而是實施多層次保護措施。在現代雲端環境中,安全不再是

Docker 儲存驅動的演進與安全性考量

在容器技術不斷發展的過程中,Docker 正從 AUFS 轉向 Overlay 作為偏好的儲存驅動。這項變革有其必要性,主要因為 AUFS 驅動已經從 Linux 核心中移除,與不再積極開發。作為一名長期關注容器技術發展的技術工作者,玄貓建議所有 AUFS 使用者在不久的將來應儘快遷移至 Overlay 驅動。

在我參與的多個企業容器化專案中,這種儲存驅動的轉換通常是平滑的,但仍需要注意潛在的效能與相容性變化。儲存驅動的選擇直接影響容器的穩定性和安全性,這也是為什麼瞭解映像檔來源的重要性變得尤為關鍵。

映像檔來源的安全保障

在使用容器技術的過程中,確保映像檔的來源安全是一項不可妥協的任務。作為一名對網路安全有深入研究的技術工作者,玄貓認為映像檔的來源驗證涉及兩個核心問題:映像檔從何處來,以及誰建立了它。

安全雜湊的基本概念

映像檔來源驗證的基礎是安全雜湊(Secure Hash)。這就像是資料的「指紋」—一個相對較小的字串,對特定資料具有唯一性。任何對原始資料的修改都會導致雜湊值發生變化。

常見的安全雜湊演算法包括:

  • SHA(有多個變體,如 SHA-256)
  • MD5(存在根本性問題,應避免使用)

當我們擁有資料的安全雜湊值和資料本身時,可以重新計算資料的雜湊值並進行比對。如果雜湊值比對,那麼我們可以確信資料沒有被破壞或篡改。

然而,一個問題依然存在:為什麼我們應該信任雜湊值本身?什麼能防止攻擊者同時修改資料和雜湊值?這個問題的最佳解決方案是加密簽名和公私鑰對。

加密簽名與發行者身份驗證

透過加密簽名,我們可以驗證映像檔發行者的身份。當發行者使用其私鑰簽署映像檔時,任何接收方都可以使用發行者的公鑰來驗證簽名,從而確認映像檔確實來自該發行者,與未經篡改。

這種機制的前提是客戶端已取得發行者的公鑰,與該公鑰未被洩露。在實際專案中,我經常看到的一個錯誤是忽略了對發行者公鑰的驗證過程,這可能導致安全漏洞。

Docker 的摘要機制

在 Docker 的術語中,安全雜湊被稱為「摘要」(Digest)。摘要是檔案系統層或清單(manifest)的 SHA256 雜湊值。清單是描述 Docker 映像檔組成部分的元資料檔案。

由於清單包含了所有映像檔層的摘要列表,如果能夠驗證清單本身未被篡改,就可以安全地下載和信任所有層,即使是透過不受信任的通道(如 HTTP)。

這種雜湊鏈結構在許多分散式系統中都有應用,如 BitTorrent 和位元幣網路,被稱為雜湊列表(hash list)。

Docker 內容信任機制

Docker 在 1.8 版本引入了內容信任(Content Trust)機制。這是 Docker 允許發行者簽署其內容的機制,完成了受信任分發機制。當使用者從儲存函式庫映像檔時,會收到包含發行者公鑰的證書,允許驗證映像檔確實來自該發行者。

當啟用內容信任時,Docker 引擎將只操作已簽署的映像檔,並拒絕執行任何簽名或摘要不比對的映像檔。

實際操作內容信任機制

讓我們透過實際操作來瞭解內容信任機制。首先,我們嘗試提取已簽署和未簽署的映像檔:

$ export DOCKER_CONTENT_TRUST=1
$ docker pull debian:wheezy
Pull (1 of 1): debian:wheezy@sha256:c584131da2ac1948aa3e66468a4424b6aea2f33a...
sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe: Pul...
4c8cbfd2973e: Pull complete
60c52dbe9d91: Pull complete
Digest: sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bdb56254c4fe
Status: Downloaded newer image for debian@sha256:c584131da2ac1948aa3e66468a4...
Tagging debian@sha256:c584131da2ac1948aa3e66468a4424b6aea2f33acba7cec0b631bd...
$ docker pull amouat/identidock:unsigned
No trust data for unsigned

在 Docker 1.8 中,必須透過設定環境變數 DOCKER_CONTENT_TRUST=1 來啟用內容信任。在 Docker 的後續版本中,這將成為預設設定。

從上面的例子可以看到,官方簽署的 Debian 映像檔已成功提取。相比之下,Docker 拒絕提取未簽署的映像檔 amouat/identidock:unsigned

推播簽署映像檔的簡單方法

推播簽署的映像檔出乎意料地簡單:

$ docker push amouat/identidock:newest
The push refers to a repository [docker.io/amouat/identidock] (len: 1)
...
843e2bded498: Image already exists
newest: digest: sha256:1a0c4d72c5d52094fd246ec03d6b6ac43836440796da1043b6ed8...
Signing and pushing trust metadata
You are about to create a new root signing key passphrase. This passphrase
will be used to protect the most sensitive key in your signing system. Please
choose a long, complex passphrase and be careful to keep the password and the
key file itself secure and backed up. It is highly recommended that you use a
password manager to generate the passphrase and keep it safe. There will be no
way to recover this key. You can find the key in your config directory.
Enter passphrase for new offline key with id 70878f1:
Repeat passphrase for new offline key with id 70878f1:
Enter passphrase for new tagging key with id docker.io/amouat/identidock ...
Repeat passphrase for new tagging key with id docker.io/amouat/identidock ...
Finished initializing "docker.io/amouat/identidock"

由於這是我第一次在啟用內容信任的情況下推播到該儲存函式庫ocker 建立了一個新的根簽名金鑰和標籤金鑰。標籤金鑰稍後會詳細解釋,但請注意保持根金鑰安全的重要性。如果丟失根金鑰,情況會變得非常複雜;沒有手動移除舊證書,儲存函式庫有使用者將無法提取新映像檔或更新現有映像檔。

現在我們可以使用內容信任來下載我們的映像檔:

$ docker rmi amouat/identidock:newest
Untagged: amouat/identidock:newest
$ docker pull amouat/identidock:newest
Pull (1 of 1): amouat/identidock:newest@sha256:1a0c4d72c5d52094fd246ec03d6b6...
sha256:1a0c4d72c5d52094fd246ec03d6b6ac43836440796da1043b6ed81ea4167eb71: Pul...
...
7e7d073d42e9: Already exists
Digest: sha256:1a0c4d72c5d52094fd246ec03d6b6ac43836440796da1043b6ed81ea4167eb71
Status: Downloaded newer image for amouat/identidock@sha256:1a0c4d72c5d52094...
Tagging amouat/identidock@sha256:1a0c4d72c5d52094fd246ec03d6b6ac43836440796d...

如果之前沒有從特定儲存函式庫映像檔,Docker 會首先檢索該儲存函式庫者的證書。這是透過 HTTPS 完成的,風險較低,但類別似於首次透過 SSH 連線到主機;您必須信任您獲得的憑證是正確的。之後從該儲存函式庫的映像檔可以使用現有證書進行驗證。

金鑰備份與管理

備份您的簽名金鑰

在多個企業安全專案中,我發現金鑰管理是最容易被忽視的環節。Docker 會加密所有靜態金鑰,並確保私人材料永遠不會寫入磁碟。由於金鑰的重要性,建議將它們備份在兩個加密 USB 儲存棒上,並存放在安全的位置。

要建立包含金鑰的 TAR 檔案,請執行:

$ umask 077
$ tar -zcvf private_keys_backup.tar.gz \
~/.docker/trust/private
$ umask 022

umask 命令確保檔案許可權設定為只讀。請注意,由於根金鑰只在建立或復原金鑰時需要,因此在不使用時可以與應該離線儲存。

標籤金鑰的角色與管理

標籤金鑰是為發行者擁有的每個儲存函式庫的。標籤金鑰由根金鑰簽名,這允許擁有發行者證書的任何使用者對其進行驗證。標籤金鑰可以在組織內分享,並用於簽署該儲存函式庫何映像檔。生成標籤金鑰後,根金鑰可以與應該離線並安全儲存。

如果標籤金鑰被洩露,仍然可以還原。透過輪換標籤金鑰,可以從系統中移除被洩露的金鑰。此過程對使用者是透明的,並且可以主動進行以防止未檢測到的金鑰洩露。

內容信任的新鮮度保證

內容信任還提供新鮮度保證,以防止重放攻擊。重放攻擊發生在當一個物件被替換為以前有效的物件時。例如,攻擊者可能用較舊的、已知有漏洞的版本替換二進位檔案,而該版本以前由發行者簽名。由於二進位檔案正確簽名,使用者可能被誘騙執行易受攻擊的二進位檔案版本。

為避免這種情況,內容信任利用與每個儲存函式庫聯的時間戳金鑰。這些金鑰用於簽署與儲存函式庫的元資料。元資料具有較短的到期日,需要由時間戳金鑰頻繁重新簽署。透過在下載映像檔之前驗證元資料未過期,Docker 客戶端可以確保接收到最新(或新鮮)的映像檔。時間戳金鑰由 Docker Hub 管理,不需要發行者的任何互動。

處理未簽署的映像檔

一個儲存函式庫同時包含已簽署和未簽署的映像檔。如果啟用了內容信任但想要下載未簽署的映像檔,請使用 --disable-content-trust 標誌:

$ docker pull amouat/identidock:unsigned
No trust data for unsigned
$ docker pull --disable-content-trust amouat/identidock:unsigned
unsigned: Pulling from amouat/identidock
...
7e7d073d42e9: Already exists
Digest: sha256:ea9143ea9952ca27bfd618ce718501d97180dbf1b5857ff33467dfdae08f57be
Status: Downloaded newer image for amouat/identidock:unsigned

如果想了解更多關於內容信任的資訊,請檢視 Docker 官方檔案,以及內容信任使用的底層規範「The Update Framework」。

內容信任的技術深度與實用性

雖然這是一個相當複雜的基礎設施,涉及多組金鑰,但 Docker 已努力確保它對終端使用者仍然簡單。透過內容信任,Docker 開發了一個使用者友好的現代安全框架,提供來源、新鮮度和完整性保證。

在我

Docker內容信任機制:確保容器映像的完整性與來源

在容器技術蓬勃發展的今日,安全性已成為組織採用Docker的關鍵考量。尤其當容器映像可能來自不同來源時,如何確保這些映像沒有被篡改並且來自可信任的發布者?Docker Content Trust正是為瞭解決這個問題而生。

Docker Content Trust的運作機制

Docker Content Trust目前在Docker Hub上已經啟用並運作中。這套機制讓使用者能夠驗證映像的完整性和來源,確保映像確實來自預期的發布者與未被竄改。對於想在本地登入檔(registry)中設定內容信任機制的組織,則需要額外設定和佈署Notary伺服器。

多年前玄貓在為某金融科技公司建置私有容器平台時,內容信任機制是我們的首要考量。畢竟,在金融領域,確保執行環境的完整性與安全性不僅是技術考量,更是合規要求。

Notary專案:更廣泛的內容信任框架

Notary是一個通用的伺服器-客戶端框架,用於以可信任與安全的方式發布和存取內容。它根據The Update Framework(TUF)規範,這個規範提供了一個安全的設計用於分發和更新內容。

Docker的內容信任框架本質上是將Notary與Docker API整合。透過同時執行登入檔和Notary伺服器,組織可以向使用者提供可信任的映像。然而,Notary的設計是獨立的,可用於各種不同的場景。

Notary的一個主要使用案例是改善常見的curl | sh安裝方式的安全性,這種方式在當前的Docker安裝指令中很典型:

$ curl -sSL https://get.docker.com/ | sh

如果這樣的下載在伺服器端或傳輸過程中被攻擊者入侵,攻擊者將能夠在受害者的電腦上執行任意命令。使用HTTPS可以阻止攻擊者在傳輸中修改資料,但他們仍可能提前結束下載,從而以潛在危險的方式截斷程式碼。

使用Notary的等效範例如下:

$ curl http://get.docker.com/ | notary verify docker.com/scripts v1 | sh

notary verify命令會比較指令碼的校驗和與Notary中docker.com可信集合中的校驗和。如果透過,我們已經驗證指令碼確實來自docker.com與未被篡改。如果失敗,Notary將終止執行,不會有資料傳遞給sh。值得注意的是,指令碼本身可以透過不安全的通道(在本例中是HTTP)傳輸而不必擔心;如果指令碼在傳輸過程中被更改,校驗和將改變並且Notary會丟擲錯誤。

未簽名映像的驗證方法

即使在使用未簽名的映像時,仍然可以透過摘要(digest)而非名稱和標籤來驗證映像:

$ docker pull debian@sha256:f43366bc755696485050ce14e1429c481b6f0ca04505c4a3093dfdb4fafb899e

這會提取撰寫本文時的debian:jessie映像。與debian:jessie標籤不同,它保證總是提取完全相同的映像(或者根本不提取)。如果摘要可以透過某種方式安全地傳輸和驗證(例如,透過來自受信任方的PGP簽名電子郵件傳送),你可以保證映像的真實性。即使在啟用內容信任的情況下,仍然可以透過摘要進行提取。

如果你不信任私有登入檔或Docker Hub來分發映像,你始終可以使用docker loaddocker save命令來匯出和匯入映像。映像可以透過內部下載站點分發或簡單地透過複製檔案。當然,如果走這條路線,你很可能會發現自己在重新建立Docker登入檔和內容信任元件的許多功能。

可重現與可信任的Dockerfile

理想情況下,Dockerfile應該每次都產生完全相同的映像。但在實踐中,這很難實作。同一個Dockerfile很可能隨著時間產生不同的映像。這顯然是一個問題,因為再次變得難以確定你的映像中包含什麼。透過在編寫Dockerfile時遵循以下規則,至少可以接近完全可重現的構建:

1. 在FROM指令中始終指定標籤

# 不好的做法,因為它提取latest標籤,預期會隨時間變化
FROM redis

# 更好的做法,但仍可能隨著小版本更新和錯誤修復而變化
FROM redis:3.0

# 最佳做法,保證每次提取完全相同的映像
FROM redis@sha256:3479bbcab384fa343b52743b933661335448f8166203688006...

使用摘要還可以防止意外損壞或篡改。

2. 安裝軟體時提供版本號

# 可以接受,因為cowsay不太可能變化
apt-get install cowsay

# 更好的做法
apt-get install cowsay=3.03+dfsg1-6

這適用於其他套件安裝工具,如pip—如果可以的話,提供版本號。如果舊套件被移除,構建將失敗,但至少這會給你警告。

值得注意的是,仍然存在一個問題:套件可能會引入依賴項,這些依賴項通常以>=條件指定,因此可能隨時間變化。要完全鎖定版本,可以看像aptly這樣的工具,它允許你對倉函式庫快照。

3. 驗證從網路下載的軟體或資料

這意味著使用校驗和或加密簽名。在這裡列出的所有步驟中,這是最重要的。如果不驗證下載,你可能會受到意外損壞和攻擊者篡改下載的影響。當軟體透過HTTP傳輸時尤其重要,因為HTTP不提供防止中間人攻擊的保證。

大多數官方映像的Dockerfile提供了使用標記版本和驗證下載的良好範例。它們通常也使用基礎映像的特定標籤,但在從套件管理器安裝軟體時不使用版本號。

在Dockerfile中安全下載軟體

在大多數情況下,廠商會提供簽名的校驗和用於驗證下載。例如,官方Node.js映像的Dockerfile包含以下內容:

RUN gpg --keyserver pool.sks-keyservers.net \
    --recv-keys 7937DFD2AB06298B2293C3187D33FF9D0246406D \
    114F43EE0176B71C7BC219DD50A3051F888C628D
ENV NODE_VERSION 0.10.38
ENV NPM_VERSION 2.10.0
RUN curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.gz" \
    && curl -SLO "http://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \
    && gpg --verify SHASUMS256.txt.asc \
    && grep " node-v$NODE_VERSION-linux-x64.tar.gz\$" SHASUMS256.txt.asc | sha256sum -c -

以上流程的工作原理是:

  1. 取得用於簽署Node.js下載的GPG金鑰
  2. 下載Node.js壓縮檔
  3. 下載壓縮檔的校驗和
  4. 使用GPG驗證校驗和是否由我們取得金鑰的人簽名
  5. 使用sha256sum工具檢查校驗和是否與壓縮檔比對

如果GPG測試或校驗和測試失敗,構建將中止。

在某些情況下,套件可在第三方倉函式庫得,這意味著可以透過新增給定倉函式庫簽名金鑰來安全安裝它們。例如,官方Nginx映像的Dockerfile包含以下內容:

RUN apt-key adv --keyserver hkp://pgp.mit.edu:80 \
    --recv-keys 573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62
RUN echo "deb http://nginx.org/packages/mainline/debian/ jessie nginx" \
    >> /etc/apt/sources.list

第一個命令取得Nginx的簽名金鑰(新增到金鑰函式庫第二個命令將Nginx套件倉函式庫到要檢查軟體的倉函式庫中。之後,可以使用apt-get install -y nginx(最好帶有版本號)簡單安全地安裝Nginx。

假設沒有簽名套件或校驗和可用,建立自己的校驗和很簡單。例如,為Redis發行版建立校驗和:

$ curl -s -o redis.tar.gz http://download.redis.io/releases/redis-3.0.1.tar.gz
$ sha1sum -b redis.tar.gz
fe1d06599042bfe6a0e738542f302ce9533dde88 *redis.tar.gz

這裡我們建立了一個160位SHA1校驗和。-b標誌告訴sha1sum工具我們處理的是二進位資料,而不是文字。

一旦你測試並驗證了軟體,可以在Dockerfile中新增如下內容:

RUN curl -sSL -o redis.tar.gz \
    http://download.redis.io/releases/redis-3.0.1.tar.gz \
    && echo "fe1d06599042bfe6a0e738542f302ce9533dde88 *redis.tar.gz" \
    | sha1sum -c -

這會將檔案下載為redis.tar.gz,並要求sha1sum驗證校驗和。如果檢查失敗,命令將失敗,構建將中止。

如果你經常發布,為每個發布版本更改所有這些細節工作量很大,因此自動化流程是值得的。在許多官方映像倉函式庫你可以找到用於此目的update.sh指令碼。

容器安全技巧

以下是確保容器佈署安全的實用技巧。並非所有建議都適用於所有佈署,但你應該熟悉可以使用的基本工具。

許多技巧描述了容器可以被限制的各種方式,以便它們無法對其他容器或主機產生不利影響。需要記住的主要問題是,主機核心的資源(CPU、記憶體、網路、UID等)在容器之間分享。如果一個容器壟斷任何這些,它將使其他容器無法獲得資源。更糟糕的是,如果容器能夠利用核心程式碼中的錯誤,它可能會使主機當機或取得對主機和其他容器的存取許可權。

設定非特權使用者

永遠不要在容器內以root身份執行生產應用程式。值得再說一次:永遠不要在容器內以root身份執行生產應用程式。攻擊者如果破壞應用程式,將完全存取容器,包括其資料和程式。更糟糕的是,成功突破容器的攻擊者將在主機上擁有root存取許可權。你不會在VM或裸機上以root身份執行應用程式,所以也不要在容器中這樣做。

為避免以root身份執行,你的Dockerfile應該始終建立一個非特權使用者,並使用USER陳述式或從入口點指令碼切換到該使用者。例如:

RUN groupadd -r user_grp && useradd -r -g user_grp user
USER user

這建立了一個名為user_grp的組和一個屬於該組的名為user的新使用者。USER陳述式將對所有後續指令生效,並在從映像啟動容器時生效。如果你需要先執行需要root許可權的操作(如安裝軟體),可能需要將USER指令延遲到Dockerfile的後面部分。

許多官方映像以相同方式建立非特權使用者,但不包含USER指令。相反,它們在入口點指令碼中使用gosu工具切換使用者。例如,官方Redis映像的入口點指令碼如下:

#!/bin/bash
set -e
if [ "$1" = 'redis-server' ]; then
    chown -R redis .
    exec gosu redis "$@"
fi
exec "$@"

此指令碼包含

使用 gosu 取代 sudo 的最佳實踐

在容器環境中選擇適當的許可權提升工具對於安全性和效能都有重大影響。我在許多企業容器佈署中發現,傳統的 sudo 工具雖然廣為人知,但實際上並不是容器環境中的最佳選擇。讓我分享為何 gosu 是更好的選擇,以及如何正確限制容器以提高安全性。

gosu vs sudo:為何容器入口指令碼中應選擇 gosu

在容器環境中使用 sudo 時,會產生一些不理想的副作用。以下是一個實際的比較:

當我們在 Ubuntu 容器中執行 sudo 指令時:

$ docker run --rm ubuntu:trusty sudo ps aux
USER PID %CPU ... COMMAND
root 1 0.0 sudo ps aux
root 5 0.0 ps aux

注意這裡有兩個執行中的程式—一個是 sudo 本身,另一個是我們實際執行的命令。

相比之下,使用 gosu:

$ docker run --rm amouat/ubuntu-with-gosu gosu root ps aux
USER PID %CPU ... COMMAND
root 1 0.0 ps aux

使用 gosu 時只有一個執行中的程式。更重要的是,該命令以 PID 1 執行,這意味著它能正確接收傳送給容器的任何訊號,而 sudo 例子中則不會。

這一點在玄貓開發的生產系統中非常關鍵,尤其是當容器需要正確處理 SIGTERM 訊號以優雅關閉時。使用 sudo 會導致訊號被 sudo 程式攔截而不是傳遞給實際應用程式,可能導致資料損失或清理不完全。

限制容器網路通訊

容器應該只開放生產環境中需要的連線埠,而與這些連線埠應該只對需要它們的其他容器開放。這一點比聽起來要複雜,因為預設情況下,容器之間可以相互通訊,無論是否明確發布或暴露了連線埠。

以下是一個實際的示範,我使用 Netcat 工具來驗證這一點:

# 啟動一個監聽 5001 連線埠的容器
$ docker run --name nc-test -d amouat/network-utils nc -l 5001

# 從另一個容器連線到第一個容器
$ docker run -e IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} nc-test) \
  amouat/network-utils sh -c 'echo -n "hello" | nc -v $IP 5001'
Connection to 172.17.0.3 5001 port [tcp/*] succeeded!

# 檢查第一個容器的日誌
$ docker logs nc-test
hello

第二個容器能夠連線到 nc-test,儘管沒有發布或暴露任何連線埠。

停用容器間通訊以提高安全性

我們可以透過在 Docker 守護程式上設定 --icc=false 標誌來改變這種行為,這將關閉容器間通訊。這可以防止被攻擊的容器能夠連線到其他容器。任何明確連線的容器仍然可以通訊。

以下是設定 --icc=false 的效果示範:

# 在 Ubuntu 上,透過設定 /etc/default/docker 中的 DOCKER_OPTS 來設定 Docker 守護程式
$ cat /etc/default/docker | grep DOCKER_OPTS=
DOCKER_OPTS="--iptables=true --icc=false"

# 啟動測試容器
$ docker run --name nc-test -d --expose 5001 amouat/network-utils nc -l 5001

# 嘗試無連結連線 - 失敗
$ docker run -e IP=$(docker inspect -f {{.NetworkSettings.IPAddress}} nc-test) \
  amouat/network-utils sh -c 'echo -n "hello" | nc -w 2 -v $IP 5001'
nc: connect to 172.17.0.10 port 5001 (tcp) timed out: Operation now in progress

# 使用連結嘗試連線 - 成功
$ docker run --link nc-test:nc-test \
  amouat/network-utils sh -c 'echo -n "hello" | nc -w 2 -v nc-test 5001'
Connection to nc-test 5001 port [tcp/*] succeeded!

# 確認連線成功
$ docker logs nc-test
hello

第一次連線失敗,因為停用了容器間通訊與沒有連結。第二次命令成功,因為我們新增了連結。

限制連線埠發布

發布連線埠到主機時,Docker 預設發布到所有介面(0.0.0.0)。你可以明確指定要繫結的介面:

$ docker run -p 87.245.78.43:8080:8080 -d myimage

這透過只允許來自指定介面的流量減少了攻擊面。

移除 Setuid/Setgid 二進位檔案

大多數容器化應用程式不需要任何 setuid 或 setgid 二進位檔案。如果我們能夠停用或移除這些二進位檔案,我們可以阻止它們被用於許可權提升攻擊。

要取得映象中此類別二進位檔案的列表,可以執行:

$ docker run debian find / -perm +6000 -type f -exec ls -ld {} \; 2> /dev/null
-rwsr-xr-x 1 root root 10248 Apr 15 00:02 /usr/lib/pt_chown
-rwxr-sr-x 1 root shadow 62272 Nov 20 2014 /usr/bin/chage
-rwsr-xr-x 1 root root 75376 Nov 20 2014 /usr/bin/gpasswd
...

然後可以使用 chmod a-s 來"去除威脅"這些二進位檔案。例如,我們可以使用以下 Dockerfile 建立一個去除威脅的 Debian 映象:

FROM debian:wheezy
RUN find / -perm +6000 -type f -exec chmod a-s {} \; || true

|| true 允許我們忽略 find 命令的任何錯誤。

構建並執行:

$ docker build -t defanged-debian .
...
$ docker run --rm defanged-debian \
  find / -perm +6000 -type f -exec ls -ld {} \; 2> /dev/null | wc -l
0

通常應在 Dockerfile 的最後步驟執行這個操作,在任何依賴 setuid/setgid 二進位檔案的操作之後,以及更改使用者之前。

限制記憶體使用

限制記憶體可以防止 DoS 攻擊和具有記憶體洩漏的應用程式,這些應用程式會慢消耗主機上的所有記憶體。

Docker 的 -m--memory-swap 標誌可以限制容器使用的記憶體和交換記憶體量。值得注意的是,--memory-swap 引數設定的是記憶體加上交換記憶體的總量,而不僅是交換記憶體。

以下是一些實際範例:

# 限制容器只能使用 128MB 記憶體,無交換記憶體
$ docker run -m 128m --memory-swap 128m amouat/stress \
  stress --vm 1 --vm-bytes 127m -t 5s
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: info: [1] successful run completed in 5s

# 嘗試使用超過限制的記憶體 - 失敗
$ docker run -m 128m --memory-swap 128m amouat/stress \
  stress --vm 1 --vm-bytes 130m -t 5s
stress: FAIL: [1] (416) <-- worker 6 got signal 9
stress: WARN: [1] (418) now reaping child worker processes
stress: FAIL: [1] (422) kill error: No such process
stress: FAIL: [1] (452) failed run completed in 0s

# 只設定 -m 標誌,--memory-swap 預設為兩倍 -m 的值
$ docker run -m 128m amouat/stress \
  stress --vm 1 --vm-bytes 255m -t 5s
stress: info: [1] dispatching hogs: 0 cpu, 0 io, 1 vm, 0 hdd
stress: info: [1] successful run completed in 5s

在第三個例子中,我們嘗試分配 255MB 記憶體,因為 --memory-swap 預設為 256MB(128MB 記憶體 + 128MB 交換),所以命令成功。

限制 CPU 使用

如果攻擊者能讓一個容器或一組容器開始使用主機上的所有 CPU,那麼攻擊者將能夠使主機上的任何其他容器資源匱乏,導致 DoS 攻擊。

在 Docker 中,CPU 份額由相對權重決定,預設值為 1,024;因此,預設情況下,所有容器將獲得相等的 CPU 份額。

以下是一個實際範例:

$ docker run -d --name load1 -c 2048 amouat/stress
$ docker run -d --name load2 amouat/stress
$ docker run -d --name load3 -c 512 amouat/stress
$ docker run -d --name load4 -c 512 amouat/stress
$ docker stats $(docker inspect -f {{.Name}} $(docker ps -q))
CONTAINER CPU % ...
/load1 392.13%
/load2 200.56%
/load3 97.75%
/load4 99.36%

在這個例子中,容器 load1 的權重為 2,048,load2 的權重為預設的 1,024,而其他兩個容器的權重為 512。在我的 8 核機器上(總共有 800% CPU 可分配),這導致 load1 獲得大約一半的 CPU,load2 獲得四分之一,load3 和 load4 各獲得八分之一。如果只執行一個容器,它將能夠取得所需的所有資源。

相對權重意味著在預設設定下,任何容器都不可能使其他容器資源匱乏。但是,你可能有一組容器在 CPU 上主導其他容器,在這種情況下,你可以為該組中的容器分配較低的預設值以確保公平性。

另外,還可以使用完全公平排程器(CFS)透過 --cpu-period--cpu-quota 標誌來分享 CPU:

$ docker run -d --cpu-period=50000 --cpu-quota=25000 myimage

這設定容器在 50000 微秒的週期內最多可以使用 25000 微秒的 CPU 時間,相當於 50% 的 CPU 使用率。

透過這些安全措施,我們可以大幅提高容器環境的安全性和穩定性。在玄貓的實踐中,我總是建議將這些限制作為容器佈署的標準設定,特別是在多租戶環境或生產系統中。

容器資源限制與安全性強化實務

在容器化環境中,適當的資源限制和安全性設定不僅能防止系統資源被濫用,也能有效降低潛在的安全風險。本文將探討 Docker 容器的資源限制與安全性強化策略,從 CPU、記憶體限制到檔案系統許可權與核心能力控制,提供全面的容器安全防護。

CPU 使用率限制實務

Docker 容器預設使用 CFS(Completely Fair Scheduler)排程器來分配 CPU 資源。以下是一個實際的例子:在一個單核心系統上,若設定容器使用一半的 CPU 資源,則該容器每 50 毫秒能使用 25 毫秒的 CPU 時間。這種精細的控制對於多容器環境的資源分配至關重要。

容器重啟策略與防止 DoS 攻擊

頻繁重啟的容器可能會耗盡系統資源,甚至導致拒絕服務(DoS)狀況。為防止這種情況,建議使用 on-failure 重啟策略,而非 always

docker run -d --restart=on-failure:10 my-flaky-image

上述指令設定容器最多重啟 10 次。目前的重啟次數可透過 docker inspect 命令檢視:

docker inspect -f "{{ .RestartCount }}" $(docker ps -lq)

值得一提的是,Docker 採用指數退避策略進行容器重啟(首次等待 100ms,然後 200ms,400ms,以此類別推),這有效防止了利用重啟功能的 DoS 攻擊。

檔案系統限制強化安全

限制容器的寫入能力可以防止多種攻擊,如寫入惡意指令碼或覆寫敏感設定檔案。從 Docker 1.5 開始,我們可以使用 --read-only 引數使容器的檔案系統唯讀:

docker run --read-only debian touch x
# 輸出: touch: cannot touch 'x': Read-only file system

對於掛載的卷,可以新增 :ro 標記使其唯讀:

docker run -v $(pwd):/pwd:ro debian touch /pwd/x
# 輸出: touch: cannot touch '/pwd/x': Read-only file system

玄貓在實際專案中發現,大多數應用程式需要寫入某些檔案。在這種情況下,建議找出應用程式需要寫入的特定資料夾和檔案,只為這些位置提供寫入許可權。

這種方法的一大優勢是簡化了稽核流程:如果容器的檔案系統與其建立時的映像檔完全相同,我們只需對映像檔進行一次離線稽核,而不必對每個容器進行單獨稽核。

限制容器核心能力

Linux 核心定義了一系列稱為「capabilities」的許可權集合,可以賦予程式更精細的系統存取許可權。這比傳統的「root vs. non-root」二元區分更為靈活。例如,ping 命令只需要開啟原始網路插槽的許可權,而不需要完整的 root 許可權。

Docker 容器預設以有限的核心能力子集執行,這意味著容器通常無法使用 GPU、音效卡或插入核心模組。若要授予容器擴充套件許可權,可以使用 --privileged 引數。

從安全形度來看,我們應該盡可能限制容器的能力。可以使用 --cap-add--cap-drop 引數控制容器可用的能力:

# 嘗試修改系統時間,預設會失敗
docker run debian date -s "10 FEB 1981 10:00:00"
# 輸出: date: cannot set date: Operation not permitted

# 新增 SYS_TIME 能力後可以修改時間
docker run --cap-add SYS_TIME debian date -s "10 FEB 1981 10:00:00"

更安全的方法是移除所有能力,然後只新增必要的能力:

# 移除所有能力後無法修改檔案擁有者
docker run --cap-drop all debian chown 100 /tmp
# 輸出: chown: changing ownership of '/tmp': Operation not permitted

# 新增 CHOWN 能力後可以修改檔案擁有者
docker run --cap-drop all --cap-add CHOWN debian chown 100 /tmp

這種方法大幅提升安全性,攻擊者即使入侵容器也會受到嚴格限制。然而,這種方法也存在一些挑戰:

  1. 難以確定哪些能力可以安全移除,通常需要透過測試確定
  2. 能力分組不夠精確,特別是 SYS_ADMIN 能力包含了大量功能

資源限制(ulimits)的應用

Linux 核心定義了可應用於程式的資源限制,如子程式數量和開放檔案描述符數量。這些限制也可應用於 Docker 容器,透過向 docker run 傳遞 --ulimit 標記,或在啟動 Docker 守護程式時使用 --default-ulimit 設定容器範圍的預設值。

以下是幾個特別有用的 ulimit 值:

CPU 時間限制

time docker run --ulimit cpu=12:14 amouat/stress stress --cpu 1

此命令限制容器最多使用 12 秒 CPU 時間,超過後會收到 SIGXCPU 訊號,達到硬限制 14 秒時會收到 SIGKILL 訊號。這對於限制代表使用者執行計算的容器特別有用。

檔案描述符限制

# 允許 5 個檔案描述符,可以正常執行
docker run --ulimit nofile=5 debian cat /etc/hostname

# 只允許 4 個檔案描述符,導致錯誤
docker run --ulimit nofile=4 debian cat /etc/hostname

設定適當的檔案描述符限制可以防止 DoS 攻擊,確保攻擊者無法讀取或寫入容器或卷。

程式數量限制

docker run --user 500 --ulimit nproc=2 -d debian sleep 100

nproc 限制使用者可以建立的最大程式數量。不過,這個限制是針對容器使用者在所有程式中的總數,而不是每個容器。這意味著如果使用相同使用者 ID 執行多個容器,它們會分享同一個程式限制。

強化核心安全

除了保持主機作業系統更新外,可以考慮執行強化的核心,使用如 grsecurity 和 PaX 提供的補丁:

  1. PaX 提供額外保護,防止攻擊者透過修改記憶體來操縱程式執行,如緩衝區溢位攻擊。它透過將記憶體中的程式碼標記為不可寫入、資料標記為不可執行來實作這一點。

  2. grsecurity 設計與 PaX 協同工作,新增與角色基礎存取控制(RBAC)、稽核和其他功能相關的補丁。

啟用這些安全增強功能可能需要自行修補和編譯核心。雖然聽起來有些嚇人,但網路上有充足的資源可供參考。

需要注意的是,這些安全增強可能導致某些應用程式無法執行,特別是 PaX 會與執行時生成程式碼的程式衝突。此外,額外的安全檢查和措施會帶來一些效能開銷。

Linux 安全模組(LSM)

Linux 核心定義了 Linux 安全模組(LSM)介面,不同模組可以實作特定的安全策略。在容器安全中,LSM 提供了額外的保護層,限制容器的系統操作許可權。

玄貓認為,雖然這些安全措施看起來繁瑣,但在構建生產環境容器時,這些限制和強化策略是不可或缺的。預設的 Docker 設定已經相當安全,但針對特定應用場景進行額外的安全強化,可以顯著降低安全風險。

在我多年的容器安全實踐中發現,最有效的安全策略是深度防禦:結合資源限制、檔案系統保護、核心能力控制和適當的監控。這些措施共同構成了一個強大的安全框架,讓容器化應用在保持敏捷性的同時不犧牲安全性。

容器安全是一個持續演進的領域,隨著容器技術的普及,安全最佳實踐也在不斷發展。定期審查和更新安全策略,關注社群的最新發展,是維護容器環境安全的關鍵。

安全強化與容器限制

SELinux與AppArmor:強化容器安全的核心模組

Linux安全模組(LSM)提供了超越標準檔案許可權控制的額外保護層,能對處理程式與使用者的存取權進行更嚴格的安全檢查。在容器環境中,主要使用兩種安全模組:Red Hat系統常用的SELinux以及Ubuntu和Debian系統常見的AppArmor。讓我們深入瞭解這兩種安全機制。

SELinux安全模組深入解析

SELinux(Security Enhanced Linux)是由美國家安全域(NSA)開發的強制存取控制(MAC)實作,與Linux標準的自主存取控制(DAC)模型有顯著不同:

  1. 型別控制機制:SELinux根據型別(本質上是標籤)來控制存取。如果SELinux政策禁止A型別的處理程式存取B型別的物件,無論檔案許可權或使用者許可權如何,這項存取都會被拒絕。SELinux檢查在標準檔案許可權檢查之後執行。

  2. 多層級安全:支援類別似政府機密分級(機密、秘密、絕密)的安全等級。較低層級的處理程式無法讀取較高層級處理程式寫入的檔案,無論檔案位置或許可權如何。這稱為多層級安全(MLS)。

  3. 多類別安全(MCS):允許為處理程式和物件套用類別,並在資源不屬於正確類別時拒絕存取。與MLS不同,類別不重疊與無層級關係。MCS可用於將資源使用限制在某型別的子集(例如,使用唯一類別可將資源限制為僅單一處理程式使用)。

在Docker環境中,SELinux預設政策旨在保護主機防範容器,以及保護容器不受其他容器影響:

# 檢查SELinux狀態
$ sestatus

# 啟用SELinux後嘗試存取掛載卷
$ mkdir data
$ echo "hello" > data/file
$ docker run -v $(pwd)/data:/data debian cat /data/file
cat: /data/file: Permission denied

# 檢查資料夾安全連貫的背景與環境
$ ls --scontext data
unconfined_u:object_r:user_home_t:s0 file

# 套用容器標籤到資料
$ chcon -Rt svirt_sandbox_file_t data
$ docker run -v $(pwd)/data:/data debian cat /data/file
hello

從Docker 1.7版本開始,在掛載卷時提供:Z:z字尾可自動重新標記卷以供容器使用:

  • :z 標記卷為所有容器可用(適用於分享卷的資料容器)
  • :Z 標記卷為僅特定容器可用
$ mkdir new_data
$ echo "hello" > new_data/file
$ docker run -v $(pwd)/new_data:/new_data:Z debian cat /new_data/file
hello

也可以使用--security-opt標記來更改容器的標籤或停用標籤功能:

$ touch newfile
$ docker run -v $(pwd)/newfile:/file --security-opt label:disable \
debian sh -c 'echo "hello" > /file'
$ cat newfile
hello

SELinux的未來發展前景看好,隨著更多的功能和預設實作加入Docker,執行SELinux安全佈署應該會變得更簡單。然而,目前SELinux的使用體驗並不理想——新手常遇到「Permission Denied」錯誤卻無法理解原因和解決方法。開發人員往往拒絕啟用SELinux,這導致開發和生產環境之間的差異——而這正是Docker原本要解決的問題。

AppArmor:簡化的安全解決方案

相較於SELinux,AppArmor的優勢和劣勢都在於其簡單性。它通常能正常運作與不幹擾使用者,但無法提供與SELinux相同的精細保護。AppArmor透過對處理程式套用設定檔來工作,限制它們在Linux功能和檔案存取層級的許可權。

Ubuntu主機很可能已經執行AppArmor。可以使用sudo apparmor_status檢查。Docker會自動為每個啟動的容器套用AppArmor設定檔,預設設定檔提供對抗容器嘗試存取各種系統資源的保護,通常位於/etc/apparmor.d/docker

如果AppArmor幹擾容器執行,可以透過傳遞--security-opt="apparmor:unconfined"docker run來為該容器關閉AppArmor。也可以透過--security-opt="apparmor:PROFILE"為容器指定不同的設定檔。

容器環境的安全稽核

在容器系統中定期進行稽核或審查是確保系統保持清潔、更新並檢查是否發生安全漏洞的好方法。容器環境的稽核應檢查所有執行的容器是否使用最新映像,以及這些映像是否使用最新與安全的軟體。

應識別並檢查容器與其建立時使用的映像之間的任何差異。此外,稽核應涵蓋其他非容器系統特定的領域,如檢查存取日誌、檔案許可權和資料完整性。如果能大量自動化稽核,就可以定期執行以儘快檢測任何問題。

玄貓認為,容器安全是DevOps流程中常被忽視的環節。僅佈署容器而不考慮其安全性,等同於在網路上放置未保護的資產。無論是使用SELinux還是AppArmor,採取額外的安全措施都能顯著降低容器遭受攻擊的風險,特別是在多租戶環境或處理敏感資料的系統中。

在實際佈署中,玄貓建議使用SELinux或AppArmor配合其他本章介紹的限制措施,形成多層防禦策略。這種深度防禦方法能確保即使一層安全控制被突破,其他層仍能提供保護,大幅降低系統整體風險。

容器安全不應被視為一次性工作,而應成為持續改進的過程。結合自動化稽核工具和定期安全檢查,可以確保容器環境始終處於最佳安全狀態,同時不妨礙開發和營運效率。

Docker容器的安全稽核策略

在容器安全管理中,定期稽核是不可或缺的環節。作為最基本的步驟,我們必須確保所有軟體都已更新至含有最新安全修補程式的版本。這項檢查應該針對每個映像檔進行,特別關注透過docker diff指令識別出的任何變更檔案。若使用了資料卷(volumes),則需要對這些目錄也進行稽核。

在實際佈署環境中,我發現大幅減少稽核工作量的有效方法是執行最小化映像檔。這些映像檔僅包含應用程式必要的檔案和函式庫幅降低了潛在的攻擊面。

# 檢查映像檔變更的基本命令
docker diff container_name

# 檢查映像檔中已安裝套件的版本
docker run --rm image_name dpkg -l   # Debian/Ubuntu系統
docker run --rm image_name rpm -qa   # CentOS/RHEL系統

主機系統同樣需要像一般主機或虛擬機器一樣進行稽核。在容器化系統中,確保核心正確修補變得更加關鍵,因為核心是在容器間分享的。

現有的容器稽核工具

目前市場上已有多種專用於容器系統稽核的工具:

  1. Docker Bench for Security - Docker官方發布的工具,能檢查系統是否符合網際網路安全中心(CIS)發布的Docker基準檔案中的各項建議。

  2. Lynis - 開放原始碼稽核工具,包含多項與Docker執行相關的檢查專案。

這些工具能協助我們自動化稽核流程,提高安全性檢查的效率與全面性。

容器事件回應策略

當安全事件發生時,Docker提供了多項功能,使我們能快速回應並調查問題根源。特別是以下命令非常有用:

  • docker commit - 快速建立受感染系統的快照
  • docker diff - 揭示攻擊者可能做出的變更
  • docker logs - 檢視可能包含攻擊痕跡的日誌

處理受感染容器時,首要問題是:「容器突破(container breakout)是否可能發生?」—即攻擊者是否可能獲得對主機系統的存取權。如果認為這種情況可能發生,則需要重新安裝主機系統並從映像檔重新建立所有容器。如果確定攻擊僅限於容器內部,則只需停止該容器並替換它。

需要特別強調的是,即使受感染的容器包含基礎映像檔中沒有的資料或變更,也絕不應該將其重新投入使用。在安全領域中,我們不能再信任這個容器。

有效的預防策略

預防攻擊的有效方法包括以某種方式限制容器,例如:

# 以唯讀檔案系統執行容器
docker run --read-only image_name

# 限制容器的Linux capabilities
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE image_name

處理完立即情況並實施某種攻擊緩解措施後,可以分析之前提交的受感染映像檔,以確定攻擊的確切原因和範圍。

若要制定有效的安全政策,建議參考CERT的「從UNIX或NT系統入侵中還原的步驟」和ServerFault網站提供的建議。

Docker的未來安全功能

Docker正在開發多項與安全相關的功能。根據Docker的優先順序,這些功能很可能在您閱讀本文時已經可用:

Seccomp(安全計算模式)

Linux的seccomp(或安全計算模式)功能可用於限制程式能夠執行的系統呼叫。Seccomp最著名的應用是在網頁瀏覽器中,包括Chrome和Firefox,用於沙箱化外掛程式。透過將seccomp與Docker整合,容器可以被限制在特定的系統呼叫集合內。

提議的Docker seccomp整合預設會拒絕32位元系統呼叫、舊網路和容器通常不需要存取的各種系統功能。此外,其他呼叫可以在執行時被明確拒絕或允許。例如,以下程式碼將允許容器進行clock_adjtime系統呼叫,這是使用網路時間協定(NTP)守護程式同步系統時間所必需的:

docker run -d --security-opt seccomp:allow:clock_adjtime ntpd

使用者名稱空間

如前所述,關於如何改善使用者名稱空間問題,特別是對於root使用者,已有幾項提案。我們可以期待很快就會看到支援將root使用者對映到主機上的非特權使用者。

此外,我預計會看到各種可用於Docker的安全工具的整合,可能採取容器安全設定檔案的形式。目前,各種安全工具和選項之間存在許多重疊(例如,檔案存取可以透過使用SELinux、降低capabilities或使用–read-only標記來限制)。

容器安全的深度防禦策略

從本章討論中可以看出,安全系統需要考慮多方面因素。最重要的建議是遵循深度防禦和最小許可權原則。這確保即使攻擊者成功入侵系統的某個元件,也無法獲得對整個系統的完全存取權,並且必須突破更多防線才能造成重大損害或存取敏感資料。

屬於不同使用者或操作敏感資料的容器群組應在與其他使用者的容器或執行公開介面的容器分開的虛擬機器中執行。容器公開的連線埠應受到嚴格控制,特別是當暴露於外部世界時,同時內部限制也能限制任何受感染容器的存取。

容器可用的資源和功能應限制為其目的所需的最小範圍,透過設定記憶體使用量、檔案系統存取和核心capabilities的限制。透過執行強化的核心和使用AppArmor或SELinux等安全模組,可以在核心層面提供進一步的安全保障。

監控與稽核的重要性

透過監控和稽核能夠及早發現攻擊。稽核在容器系統中特別有趣,因為容器可以輕鬆地與它們建立的映像檔進行比較,以檢測可疑的變更。同時,可以離線檢查映像檔,確保它們執行最新與安全的軟體版本。沒有狀態的受感染容器可以迅速替換為新版本。

容器在安全方面是一種積極因素,這得益於它們提供的額外隔離和控制層級。一個正確使用容器的系統將比沒有容器的等效系統更加安全。

在我多年的容器安全實踐中,深刻體會到容器技術並非天生安全,而是提供了實作更安全環境的工具。關鍵在於如何正確利用這些工具,實施多層次的安全策略,並保持續的監控與更新。只有這樣,我們才能真正發揮容器技術在安全方面的優勢。

容器佈署最佳實踐:從基礎設定到生產環境安全考量

在現代軟體開發中,容器已成為應用程式佈署的重要根本。本文將探討容器佈署的各個關鍵導向,從基礎設定到進階的生產環境設定,協助開發者建立穩定、安全與高效的容器化系統。

佈署選項與執行管理

容器佈署有多種執行選項,每種選項各有優缺點。在玄貓多年的容器化專案經驗中,我發現選擇正確的佈署策略對系統穩定性至關重要。

Shell指令碼佈署 - 簡單但有限

最基本的佈署方式是使用shell指令碼。這種方式的優點是簡單直接,適合小型專案或開發環境:

#!/bin/bash
# 簡單的容器佈署指令碼
docker pull myapp:latest
docker stop myapp-container || true
docker rm myapp-container || true
docker run -d --name myapp-container -p 8080:80 myapp:latest

這種方法雖然直觀,但缺乏監控和自動重啟機制,不適合生產環境使用。當容器因任何原因停止時,需要手動干預才能重新啟動。

進階佈署 - 使用Process Manager

對於生產環境,建議使用專業的程式管理器,如systemd。systemd能提供更完善的容器生命週期管理,包括啟動順序控制、自動重啟和日誌管理。

以下是一個systemd服務檔案範例:

[Unit]
Description=My Application Container
After=docker.service
Requires=docker.service

[Service]
Restart=always
ExecStartPre=-/usr/bin/docker stop myapp-container
ExecStartPre=-/usr/bin/docker rm myapp-container
ExecStart=/usr/bin/docker run --name myapp-container -p 8080:80 myapp:latest
ExecStop=/usr/bin/docker stop myapp-container

[Install]
WantedBy=multi-user.target

在此設定中,systemd會確保容器在Docker服務啟動後開始執行,並在容器停止時自動重啟。這為生產環境提供了必要的穩定性。

使用設定管理工具

對於更複雜的佈署,Configuration Management (CM) 工具如Ansible、Chef或Puppet提供了更強大的自動化能力:

# Ansible佈署範例
- name: 佈署應用容器
  hosts: app_servers
  tasks:
    - name: 提取最新映像
      docker_image:
        name: myapp
        tag: latest
        source: pull
        
    - name: 啟動應用容器
      docker_container:
        name: myapp-container
        image: myapp:latest
        state: started
        restart_policy: always
        ports:
          - "8080:80"
        env:
          DB_HOST: "{{ db_host }}"
          API_KEY: "{{ api_key }}"

這種方法允許更精細的控制,並能輕鬆管理多台主機上的容器佈署。CM工具還能處理環境特定的設定變數,使同一佈署指令碼可用於不同環境。

使用Docker Machine進行資源佈建

Docker Machine是一個強大的工具,可自動化虛擬機器的建立和設定,特別適合多雲環境:

# 在AWS上建立Docker主機
docker-machine create --driver amazonec2 \
  --amazonec2-access-key $AWS_ACCESS_KEY \
  --amazonec2-secret-key $AWS_SECRET_KEY \
  --amazonec2-region ap-northeast-1 \
  production-host

# 切換到新主機環境
eval $(docker-machine env production-host)

# 現在所有docker命令都在遠端主機上執行
docker run -d -p 80:80 myapp:latest

Docker Machine不僅簡化了基礎設施佈建,還提供了一致的環境管理方式,無論底層是本地虛擬機器、AWS、Azure還是其他雲端服務提供商。

代理與負載平衡

在分散式系統中,代理伺服器扮演著關鍵角色。我經常使用nginx作為容器前端的反向代理:

# nginx反向代理設定
server {
    listen 80;
    server_name example.com;
    
    location / {
        proxy_pass http://app-container:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

對於動態環境,可以使用dockerize或docker-gen等工具自動更新代理設定:

docker run -d \
  -v /var/run/docker.sock:/tmp/docker.sock:ro \
  -v /path/to/templates:/etc/docker-gen/templates:ro \
  -v /etc/nginx:/etc/nginx \
  jwilder/docker-gen \
  -notify-sighup nginx \
  /etc/docker-gen/templates/nginx.tmpl /etc/nginx/conf.d/default.conf

這種設定允許代理伺服器根據容器的建立和停止自動調整設定,實作零停機佈署。

主機設定最佳實踐

選擇合適的主機作業系統

在生產環境中,主機OS的選擇至關重要。我推薦使用專為容器設計的精簡發行版,如CoreOS或RancherOS,它們移除了不必要的元件,降低了安全風險和維護負擔。

儲存驅動選擇

儲存驅動對容器效能有顯著影響。在生產環境中,我建議根據工作負載特性選擇適當的驅動:

  • overlay2:適合大多數場景,提供良好的效能和相容性
  • devicemapper:在需要直接LVM控制的環境中較為適用
  • btrfs/zfs:當需要高階檔案系統功能如快照時

以下是設定devicemapper的範例:

{
  "storage-driver": "devicemapper",
  "storage-opts": [
    "dm.thinpooldev=/dev/mapper/docker-thinpool",
    "dm.use_deferred_removal=true",
    "dm.use_deferred_deletion=true"
  ]
}

專業容器代管平台

對於企業級佈署,專業代管平台提供了額外的管理便利性:

Triton

Joyent的Triton將容器直接執行在裸機上,消除了虛擬機器層,提供卓越效能:

# 設定Triton環境
eval "$(triton env)"

# 佈署容器
triton instance create \
  --name webserver \
  --network public \
  --image ubuntu-certified-16.04 \
  myapp:latest

Google Container Engine (GKE)

GKE提供了管理Kubernetes叢集的簡便方式:

# 建立GKE叢集
gcloud container clusters create my-cluster \
  --num-nodes=3 \
  --machine-type=n1-standard-2 \
  --zone=asia-east1-a

# 佈署應用
kubectl create -f application.yaml

Amazon ECS

AWS的ECS提供了與AWS生態系統深度整合的容器協調服務:

{
  "family": "webservice",
  "containerDefinitions": [
    {
      "name": "web",
      "image": "nginx:latest",
      "essential": true,
      "portMappings": [
        {
          "containerPort": 80,
          "hostPort": 80
        }
      ],
      "memory": 512,
      "cpu": 10
    }
  ]
}

ECS任務定義支援多種AWS服務整合,包括負載平衡、自動擴充套件和IAM角色。

持久資料管理與生產容器的無狀態特性使其易於擴充套件,但許多應用需要持久儲存。以下是幾種管理持久資料的方法:

使用資料卷

Docker資料卷提供了容器外的持久儲存:

# 建立命名卷
docker volume create pgdata

# 使用卷啟動容器
docker run -d \
  --name postgres \
  -v pgdata:/var/lib/postgresql/data \
  postgres:latest

使用主機繫結掛載

對於需要特定主機路徑的應用:

docker run -d \
  --name nginx \
  -v /host/path/to/html:/usr/share/nginx/html:ro \
  -p 80:80 \
  nginx:latest

只讀掛載(:ro)可增強安全性,防止容器修改主機檔案。

安全分享敏感資料

在容器化環境中安全地分享敏感資料是一項挑戰。以下是幾種方法:

環境變數

最簡單但安全性較低的方法是使用環境變數:

docker run -d \
  --name app \
  -e API_KEY=secret_value \
  -e DB_PASSWORD=db_password \
  myapp:latest

但這種方法存在環境變數可能被記錄的風險。

Docker Secrets

在Swarm模式下,Docker Secrets提供了更安全的選擇:

# 建立金鑰
echo "my_secret_password" | docker secret create db_password -

# 使用金鑰
docker service create \
  --name app \
  --secret db_password \
  myapp:latest

在容器內,金鑰以檔案形式掛載在/run/secrets/目錄下。

專用金鑰管理工具

對於更高安全要求,可使用Vault或KeyWhiz等專用金鑰管理工具:

# 使用Vault儲存金鑰
vault write secret/app/db_password value=my_secret_password

# 容器啟動時取得金鑰
DB_PASSWORD=$(vault read -field=value secret/app/db_password)
docker run -d --name app -e DB_PASSWORD=$DB_PASSWORD myapp:latest

容器網路與生產登入檔

容器網路設定

生產環境中的容器網路需要考慮安全性、可擴充套件性和效能:

# 建立自定義網路
docker network create --driver overlay --subnet 10.0.0.0/24 app-network

# 將容器連線到網路
docker run -d --name app --network app-network myapp:latest

overlay網路驅動支援跨主機通訊,適合分散式應用。

生產級映像登入檔

對於企業環境,私有登入檔至關重要:

# 啟動安全的私有登入檔
docker run -d \
  --name registry \
  -p 5000:5000 \
  -v /path/to/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

使用TLS加密和身分驗證保護登入檔安全。

持續佈署與交付

容器化應用特別適合持續佈署/交付(CD)流程:

# Jenkins Pipeline範例
pipeline {
    agent any
    stages {
        stage('Build') {
            steps {
                sh 'docker build -t myapp:$BUILD_NUMBER .'
            }
        }
        stage('Test') {
            steps {
                sh 'docker run --rm myapp:$BUILD_NUMBER npm test'
            }
        }
        stage('Deploy') {
            steps {
                sh '''
                docker tag myapp:$BUILD_NUMBER registry.example.com/myapp:latest
                docker push registry.example.com/myapp:latest
                ssh production "docker pull registry.example.com/myapp:latest && \
                               docker-compose up -d"
                '''
            }
        }
    }
}

這種流程實作了程式碼提交後的自動測試和佈署,大幅提高了開發效率和佈署頻率。

容器安全最佳實踐

容器安全是一個多層面的挑戰,需要從映像構建到執行時保護的全面考量:

映像安全

  • 使用精簡基礎映像:採用Alpine等輕量級映像減少攻擊面
  • 定期更新基礎映像:保持依賴包的最新安全補丁
  • 掃描映像漏洞:使用Clair等工具檢測安全問題

執行時保護

限制容器資源和許可權是基本的安全實踐:

# 限制資源使用
docker run -d \
  --name restricted-app \
  --memory=512m \
  --cpu-shares=512 \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --security-opt=no-new-privileges \
  myapp:latest

上述命令限制了記憶體使用、CPU份額,並移除了所有Linux capabilities,只保留繫結低連線埠的能力。

網路隔離

適當的網路隔離可防止橫向移動攻擊:

# 建立隔離網路
docker network create --internal private-network

# 僅內部存取的容器
docker run -d --name db --network private-network postgres:latest

# 可對外的前端容器
docker run -d \
  --name web \
  --network private-network \
  --network public-network \
  -p 80:80 \
  webapp:latest

這種設定確保資料函式庫不直接暴露於外部網路。

在容器佈署過程中,平衡開發便利性與生產環境安全性是一項持續的挑戰。透過採用適當的工具和流程,

Docker 實戰最佳實踐:安全性與服務佈署

安全性是容器技術的關鍵挑戰

在容器化應用程式的世界中,安全性一直是最受關注也最容易被忽略的環節。多年來,玄貓在協助企業匯入 Docker 的過程中發現,許多開發團隊在追求快速佈署的同時,往往將安全性視為「事後才考慮」的問題。這種思維在現代雲原生架構中尤其危險。

安全性不僅是一個附加功能,它應該是容器化戰略的核心部分。Docker 提供了許多內建的安全機制,但如何正確設定和應用這些機制,需要深入理解容器技術的工作原理。

容器安全的關鍵考量因素

容器安全涉及多個層面,從基礎的名稱空間隔離到更複雜的防禦深度策略。以下是幾個我認為最重要的關注點:

  1. 隔離性與名稱空間:容器透過 Linux 名稱空間技術提供了程式隔離,但這種隔離與傳統虛擬機器相比仍有其限制
  2. 最小許可權原則:容器應該只擁有完成其任務所需的最小許可權集
  3. 防禦深度:實施多層次的安全措施,確保即使一層被突破,其他層仍可提供保護
  4. 映像檔來源可信度:確保使用的容器映像檔來源可靠與內容可信

在深入技術細節前,讓我們先了解 Docker 安全架構的基本概念。

Docker 安全架構基礎

Docker 的安全模型建立在 Linux 核心安全機制之上,包括名稱空間、控制群組 (cgroups) 以及其他安全模組。每個容器都在自己的名稱空間中執行,擁有獨立的程式空間、網路堆積積疊和掛載點。

然而,與常見的誤解不同,容器並不提供完全的安全隔離。所有容器分享同一個主機核心,這意味著核心漏洞可能影響所有容器。正如我在一次金融科技公司的安全稽核中發現的,這種分享核心模型是容器與虛擬機器最根本的安全差異。

Docker daemon 的安全考量

Docker daemon 擁有 root 許可權,這是一個重要的安全考量點。如果攻擊者能夠控制 Docker daemon,他們基本上就獲得了主機的完全控制權。因此,限制對 Docker daemon 的存取至關重要。

# 限制 Docker socket 的存取許可權
sudo chmod 660 /var/run/docker.sock
sudo chown root:docker /var/run/docker.sock

這個簡單的設定可確保只有 root 使用者和 docker 群組的成員能夠與 Docker daemon 通訊。在我的實踐中,我總是建議客戶建立專用的服務帳戶來執行 Docker 相關操作,而不是依賴 root 使用者。

實戰案例:Identidock 應用的安全強化

為了說明 Docker 安全實踐,讓我們以一個名為 Identidock 的簡單應用為例,探討如何應用安全最佳實踐。

初始狀態評估

當我第一次審查 Identidock 應用時,它存在幾個常見的安全問題:

  1. 容器以 root 身份執行
  2. 沒有資源限制
  3. 使用了未驗證的基礎映像檔
  4. 未移除不必要的 setuid/setgid 二進位檔案

安全強化步驟

1. 設定非 root 使用者執行容器

在 Dockerfile 中新增 USER 指令是一個簡單但有效的安全措施:

FROM python:3.8-slim

RUN pip install flask==2.0.1 uWSGI==2.0.19.1 requests==2.26.0 redis==3.5.3 && \
    groupadd -r uwsgi && useradd -r -g uwsgi uwsgi && \
    mkdir -p /opt/identidock && \
    rm -rf /var/cache/apt/*

WORKDIR /opt/identidock

COPY app /opt/identidock

# 確保非 root 使用者可以存取應用目錄
RUN chown -R uwsgi:uwsgi /opt/identidock

# 切換到非 root 使用者
USER uwsgi

CMD ["uwsgi", "--http", "0.0.0.0:9090", "--wsgi-file", "/opt/identidock/identidock.py", "--callable", "app", "--stats", "0.0.0.0:9191"]

將容器設定為以非 root 使用者執行是最基本也最重要的安全措施之一。即使容器被入侵,攻擊者也沒有 root 許可權,這大限制了潛在的損害範圍。

2. 應用資源限制

為容器設定資源限制可以防止 DoS 攻擊和資源耗盡問題:

docker run -d --name identidock \
  --memory="512m" \
  --memory-swap="512m" \
  --cpu-shares=512 \
  --pids-limit=100 \
  --security-opt="no-new-privileges" \
  example/identidock

這些限制確保了容器只能使用分配給它的資源,防止了一個容器影響主機上其他容器的效能。

3. 移除不必要的 setuid/setgid 二進位檔案

setuid 和 setgid 二進位檔案允許以擁有者或群組許可權執行程式,這可能成為許可權提升的途徑。我們可以在 Dockerfile 中新增以下命令來移除它們:

RUN for i in $(find / -perm +6000 -type f -exec ls -ld {} \; | grep -v "^lrwx"); do \
    chmod a-s $i; \
done

這個命令會找出所有具有 setuid 或 setgid 位的檔案,並移除這些特殊許可權。

進階安全措施:Linux 安全模組 (LSMs)

除了基本的安全設定外,Docker 還支援更高階的安全機制,包括 AppArmor 和 SELinux 等 Linux 安全模組。

AppArmor 設定

AppArmor 是一個強大的 Linux 安全模組,可以限制容器的能力:

# 建立一個 AppArmor 設定檔案
cat > /etc/apparmor.d/docker-identidock << EOF
#include <tunables/global>

profile docker-identidock flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>
  
  network,
  capability,
  file,
  umount,
  
  deny /etc/passwd r,
  deny /etc/shadow r,
  
  deny @{PROC}/* w,
  deny mount,
  deny /sys/[^f]*/** wklx,
  deny /sys/f[^s]*/** wklx,
  deny /sys/fs/[^c]*/** wklx,
  deny /sys/fs/c[^g]*/** wklx,
  deny /sys/fs/cg[^r]*/** wklx,
  
  /opt/identidock/** r,
  /var/lib/redis/** r,
}
EOF

# 載入 AppArmor 設定
sudo apparmor_parser -r -W /etc/apparmor.d/docker-identidock

# 使用 AppArmor 設定執行容器
docker run -d --name identidock \
  --security-opt="apparmor:docker-identidock" \
  example/identidock

這個 AppArmor 設定嚴格限制了容器的檔案系統存取許可權,只允許它讀取特定目錄,並明確禁止存取敏感檔案如 /etc/passwd 和 /etc/shadow。

SELinux 設定

對於使用 RedHat 系列系統的環境,SELinux 是另一個強大的安全工具:

# 啟用 SELinux 
setenforce 1

# 執行帶有 SELinux 標籤的容器
docker run -d --name identidock \
  --security-opt label=type:container_t \
  example/identidock

SELinux 提供了比傳統 Linux 存取控制更細粒度的安全策略。它根據類別強制存取控制 (Type Enforcement),為容器提供了額外的安全邊界。

Docker 內容信任 (Docker Content Trust)

Docker 內容信任是一個確保映像檔完整性和來源可靠性的機制。它根據 Notary 專案,使用數位簽名來驗證映像檔的發布者身份和內容完整性。

啟用 Docker 內容信任

# 啟用 Docker 內容信任
export DOCKER_CONTENT_TRUST=1

# 推播簽名映像檔
docker push example/identidock:latest

啟用 Docker 內容信任後,Docker 會自動簽署你推播的映像檔,並在提取映像檔時驗證其簽名。這確保了只有經過授權的發布者才能更新映像檔,與映像檔在傳輸過程中未被篡改。

使用摘要 (Digest) 確保映像檔完整性

即使不使用 Docker 內容信任,我們也可以透過映像檔摘要來確保映像檔的完整性:

# 提取特定摘要的映像檔
docker pull example/identidock@sha256:a1b2c3d4e5f6...

摘要是映像檔內容的加密雜湊,可以確保你每次提取的都是完全相同的映像檔。

服務發現:容器協調的關鍵

在現代微服務架構中,服務發現是一個關鍵元件,它使服務能夠動態地找到並連線其他服務。

CAP 定理與服務發現

在討論服務發現系統時,理解 CAP 定理很重要。CAP 定理指出,在分散式系統中,一致性 (Consistency)、可用性 (Availability) 和分割槽容忍性 (Partition tolerance) 三者無法同時滿足。

服務發現系統通常需要在這三者之間做出取捨。例如,Consul 在面對網路分割槽時會優先保證一致性,而其他系統如 Eureka 則可能優先保證可用性。

Consul 服務發現實戰

Consul 是一個廣泛使用的服務發現解決方案,它提供了服務註冊、健康檢查和 KV 儲存等功能。以下是如何使用 Consul 進行服務發現的範例:

# 啟動 Consul 容器
docker run -d --name consul \
  -p 8500:8500 \
  -p 8600:8600/udp \
  consul agent -server -bootstrap -ui -client=0.0.0.0

# 註冊服務到 Consul
curl -X PUT -d '{
  "ID": "identidock-1",
  "Name": "identidock",
  "Tags": ["v1", "production"],
  "Address": "10.0.0.10",
  "Port": 9090,
  "Check": {
    "HTTP": "http://10.0.0.10:9090/health",
    "Interval": "10s"
  }
}' http://localhost:8500/v1/agent/service/register

這個範例展示瞭如何手動註冊服務到 Consul。在實際環境中,我們通常會使用容器化的 Registrator 或 Consul 代理來自動註冊服務。

使用 etcd 進行服務發現

etcd 是另一個流行的分散式 KV 儲存,常用於服務發現:

# 啟動 etcd 容器
docker run -d --name etcd \
  -p 2379:2379 \
  -p 2380:2380 \
  quay.io/coreos/etcd:v3.4.13 \
  /usr/local/bin/etcd \
  --advertise-client-urls http://0.0.0.0:2379 \
  --listen-client-urls http://0.0.0.0:2379

# 註冊服務到 etcd
curl -X PUT -d '{"host": "10.0.0.10", "port": 9090}' \
  http://localhost:2379/v2/keys/services/identidock/instance1

etcd 的 API 相對簡單,使其成為服務發現的絕佳選擇,特別是在 Kubernetes 等系統中。

SkyDNS:根據 DNS 的服務發現

SkyDNS 提供了根據 DNS 的服務發現,它通常與 etcd 結合使用:

# 啟動 SkyDNS 容器
docker run -d --name skydns \
  --link etcd:etcd \
  -p 53:53/udp \
  skynetservices/skydns:2.5.2a \
  -machines=http://etcd:2379 \
  -domain=local.

# 查詢服務
dig @localhost identidock.services.local. SRV

DNS 是一個成熟與廣泛支援的協定,使其成為服務發現的自然選擇。然而,它也有一些限制,如快取問題和有限的查詢類別。