摘要: 使用QT進行SCSI指令操作時遇到問題,0x28讀取正常,但0x2A寫入失敗,原因是系統對0x2A命令的寫入許可權控制嚴格。解決方法是通過FSCTL_LOCK_VOLUME實現獨占訪問,實現對USB設備的寫操作。 問題參考:https://blog.csdn.net/kifea/article ...
7 系統配置:日誌、系統時間、批處理作業和用戶
當你第一次進入 /etc 目錄查看系統配置時,可能會感到有些不知所措。好在雖然你看到的大多數文件都會在一定程度上影響系統的運行,但只有少數文件是基本文件。
本章將介紹系統中使第 4 章中討論的基礎架構可供用戶空間軟體使用的部分,這些軟體通常與我們交互,例如第 2 章中介紹的工具。我們將特別關註以下內容:
- 系統日誌
- 系統庫訪問以獲取伺服器和用戶信息的配置文件
- 系統啟動時運行的一些選定的伺服器程式(有時稱為守護進程,可用於調整伺服器程式和配置文件的配置實用程式
- 時間配置
- 定期任務調度
systemd 的廣泛使用減少了典型 Linux 系統中基本、獨立守護進程的數量。系統日誌(syslogd)守護進程就是一個例子,它的功能現在主要由 systemd 內置的一個守護進程(journald)提供。但仍有一些傳統守護進程保留了下來,如 crond 和 atd。
與前幾章一樣,本章幾乎沒有網路方面的內容,因為網路是系統的一個獨立構件。在第 9 章中,你將看到網路的作用。
7.1 系統日誌
大多數系統程式都會將診斷輸出作為信息寫入 syslog 服務。傳統的 syslogd 守護進程通過等待消息併在收到消息後將其發送到適當的通道(如文件或資料庫)來執行這項服務。在大多數現代系統中,大部分工作由 journald(systemd 自帶)完成。雖然我們在本書中將重點討論 journald,但我們也會涉及傳統系統日誌的許多方面。
系統日誌是系統中最重要的部分之一。當出現問題而不知從何入手時,查看日誌總是明智之舉。如果使用 journald,則可以使用 journalctl 命令來查看日誌,我們將在第 7.1.2 節介紹該命令。在舊系統上,則需要檢查文件本身。無論哪種情況,日誌信息都是這樣的
Aug 19 17:59:48 duplex sshd[484]: Server listening on 0.0.0.0 port 22.
日誌信息通常包含進程名稱、進程 ID 和時間戳等重要信息。此外還有兩個欄位:facility(一般類別)和 severity(信息的緊急程度)。稍後我們將詳細討論這兩個欄位。
由於新舊軟體組件的組合各不相同,在 Linux 系統中理解日誌記錄可能有些難度。一些發行版(如 Fedora)已改用僅日誌記錄的預設設置,而另一些發行版則在運行 journald 的同時運行舊版 syslogd(如 rsyslogd)。較早的發行版和某些專用系統可能根本不使用 systemd,而只使用其中一個 syslogd 版本。此外,有些軟體系統會完全繞過標準化日誌,自己編寫日誌。
7.1.1 檢查日誌設置
您應該檢查自己的系統,看看安裝了何種日誌記錄。方法如下:
- 檢查 journald,如果運行的是 systemd,則幾乎肯定安裝了 journald。雖然可以在進程列表中查找 journald,但最簡單的方法還是運行 journalctl。如果 journald 在系統中處於活動狀態,你就會看到日誌信息的分頁列表。
- 檢查 rsyslogd。在進程列表中查找 rsyslogd,並查看 /etc/rsyslog.conf。如果沒有 rsyslogd,請查看 syslog-ng(syslogd 的另一個版本),查找名為 /etc/syslog-ng 的目錄。
- 繼續在 /var/log 中查找日誌文件。如果使用的是 syslogd 版本,該目錄應該包含許多文件,其中大部分由 syslog 守護進程創建。不過,這裡也會有一些由其他服務維護的文件,比如 wtmp 和 lastlog,它們是 last 和 lastlog 等實用程式訪問的日誌文件,用於獲取登錄記錄。
此外,/var/log 中可能還有更多包含日誌的子目錄。這些日誌幾乎總是來自其他服務。其中的 /var/log/journal 就是 journald 存儲其(二進位)日誌文件的地方。
參考資料
- 軟體測試精品書籍文檔下載持續更新 https://github.com/china-testing/python-testing-examples 請點贊,謝謝!
- 本文涉及的python測試開發庫 謝謝點贊! https://github.com/china-testing/python_cn_resouce
- python精品書籍下載 https://github.com/china-testing/python_cn_resouce/blob/main/python_good_books.md
- Linux精品書籍下載 https://www.cnblogs.com/testing-/p/17438558.html
7.1.2 搜索和監控日誌
除非系統中沒有 journald,或要搜索由其他工具維護的日誌文件,否則都要查看日誌。在沒有參數的情況下,journalctl 訪問工具就像消防水龍頭,提供日誌中的所有信息,從最舊的開始(就像它們在日誌文件中一樣)。值得慶幸的是,journalctl 預設使用 less 等尋呼機來顯示消息,因此你的終端不會被淹沒。你可以使用日誌記錄器搜索信息,也可以用 journalctl -r 來反轉信息的時間順序,但還有更好的方法來查找日誌。
註意:要完全訪問日誌信息,需要以 root 或 adm 或 systemd-journal 組用戶身份運行 journalctl。大多數發行版的預設用戶都有訪問許可權。
一般來說,只需在命令行中添加日誌的個別欄位,就能搜索日誌信息;例如,運行 journalctl _PID=8792 搜索來自進程 ID 8792 的日誌信息。不過,最強大的過濾功能更具有通用性。如果需要多個條件,可以指定一個或多個。
- 按時間過濾
在縮小特定時間範圍方面,-S(自)選項是最有用的選項之一。下麵是一個最簡單有效的使用示例:
$ journalctl -S -4h
$ journalctl -S 06:00:00
$ journalctl -S 2020-01-14
$ journalctl -S "2020-01-14 14:30:00
-4h 部分看起來像一個選項,但實際上是一個時間規範,告訴 journalctl 在當前時區搜索過去四小時內的郵件。你也可以使用特定日期和/或時間的組合:
-U(直到)選項的作用與此相同,它指定了 journalctl 檢索信息的截止時間。不過,這個選項通常沒那麼有用,因為你通常會翻頁或搜索信息,直到找到需要的內容,然後退出。
- 按單元過濾
另一種快速有效的獲取相關日誌的方法是按 systemd 單元過濾。你可以使用 -u 選項這樣做:
$ journalctl -u cron.service
按單元過濾時,通常可以省略單元類型(本例中為.service)。
如果不知道特定單元的名稱,可嘗試使用此命令列出日誌中的所有單元:
journalctl -F _SYSTEMD_UNIT
選項 -F 顯示日誌中特定欄位的所有值。
- 查找欄位
有時,你只需知道要搜索哪個欄位。您可以按如下方式列出所有可用欄位:
$ journalctl -N
任何以下劃線開頭的欄位(如上例中的 _SYSTEMD_UNIT)都是受信任欄位;發送報文的客戶端不能更改這些欄位。
- 文本過濾
搜索日誌文件的經典方法是在所有日誌文件中運行 grep,希望在文件中找到相關行或可能存在更多信息的位置。同樣,你也可以使用 -g 選項,通過正則表達式搜索日誌信息,如本例所示,它會返回包含 kernel 的信息,然後是記憶體:
journalctl -g 'kernel.*memory'
遺憾的是,用這種方法搜索日誌時,只能得到與表達式匹配的信息。通常,重要信息可能就在時間附近。試著從匹配信息中找出時間戳,然後運行 journalctl -S,查看同一時間有哪些信息。
註意:-g選項要求使用特定庫構建 journalctl。某些發行版不包含支持 -g 的版本。
- 按啟動過濾
通常情況下,你會發現自己在查看日誌時,會看到機器啟動時或宕機(重啟)前的信息。只需一次啟動,從機器啟動到停止,就能輕鬆獲取信息。例如,如果要查找當前啟動的開始時間,只需使用 -b 選項即可:
journalctl -g 'kernel.*memory'
也可以添加偏移量,例如,要從上一次啟動開始,偏移量為-1。
$ journalctl -b -1
註意
結合 -b 和 -r(反向)選項,可以快速檢查機器是否在最後一個周期乾凈利落地關機。試一試;如果輸出與此處示例相似,則說明關機是乾凈的:
$ journalctl -r -b -1
-- Logs begin at Wed 2019-04-03 12:29:31 EDT, end at Fri 2019-08-02 19:10:14 EDT. --
Jul 18 12:19:52 mymachine systemd-journald[602]: Journal stopped
Jul 18 12:19:52 mymachine systemd-shutdown[1]: Sending SIGTERM to remaining processes...
Jul 18 12:19:51 mymachine systemd-shutdown[1]: Syncing filesystems and block devices.
你也可以通過 IDs 來查看引導,而不是像 -1 這樣的偏移量。運行以下命令獲取啟動 ID:
$ journalctl --list-boots
-1 e598bd09e5c046838012ba61075dccbb Fri 2019-03-22 17:20:01 EDT—Fri 2019-04-12 08:13:52 EDT
0 5696e69b1c0b42d58b9c57c31d8c89cc Fri 2019-04-12 08:15:39 EDT—Fri 2019-08-02 19:17:01 EDT
最後,你可以使用 journalctl -k 顯示內核信息(無論是否選擇特定引導)。
- 按嚴重性/優先順序過濾
有些程式會產生大量診斷信息,從而掩蓋重要日誌。通過在 -p 選項中指定一個介於 0(最重要)和 7(最不重要)之間的值,可以按嚴重程度過濾日誌。例如,要獲取 0 至 3 級的日誌,請運行
$ journalctl --list-boots
-1 e598bd09e5c046838012ba61075dccbb Fri 2019-03-22 17:20:01 EDT—Fri 2019-04-12 08:13:52 EDT
0 5696e69b1c0b42d58b9c57c31d8c89cc Fri 2019-04-12 08:15:39 EDT—Fri 2019-08-02 19:17:01 EDT
如果只想要一組特定嚴重性級別的日誌,請使用 ... range 語法:
$ journalctl -p 2..3
按嚴重性過濾聽起來可以節省很多時間,但可能用處不大。大多數應用程式預設情況下都不會生成大量信息數據,不過有些應用程式會提供配置選項來啟用更詳細的日誌記錄。
- 簡單的日誌監控
監控日誌的一種傳統方法是在日誌文件中使用 tail -f 或 less follow 模式 (less +F),查看系統日誌記錄器發送的信息。這並不是一種非常有效的常規系統監控方法(太容易遺漏某些內容),但在試圖查找問題或實時仔細查看啟動和運行情況時,它對檢查服務非常有用。
由於 journald 不使用純文本文件,因此使用 tail -f 並不奏效;相反,可以使用 journalctl 的 -f 選項,在日誌到達時列印日誌,效果與此相同:
$ journalctl -f
這種簡單的調用足以滿足大多數需求。不過,如果您的系統有大量與您的需求無關的日誌信息,您可能需要添加前面提到的一些過濾選項。
7.1.3 日誌文件輪換
使用 syslog 守護進程時,系統記錄的任何日誌信息都會進入日誌文件,這意味著需要偶爾刪除舊信息,以免它們最終占用所有存儲空間。不同的發行版有不同的方法,但大多數都使用 logrotate 實用程式。
這種機制被稱為日誌輪轉。由於傳統的文本日誌文件在開頭包含最舊的信息,在結尾包含最新的信息,因此很難只刪除文件中的舊信息來釋放空間。相反,由 logrotate 維護的日誌會被分成很多塊。
例如,在 /var/log 中有一個名為 auth.log 的日誌文件,其中包含最新的日誌信息。然後是 auth.log.1、auth.log.2 和 auth.log.3,每個文件都包含逐漸變舊的數據。當 logrotate 認為是時候刪除一些舊數據時,它會這樣 “輪換 ”這些文件:
- 刪除最老的文件 auth.log.3。
- 將 auth.log.2 重命名為 auth.log.3。
- 將 auth.log.1 重命名為 auth.log.2。
- 將 auth.log 重命名為 auth.log.1。
不同發行版的名稱和一些細節有所不同。例如,在 Ubuntu 配置中,logrotate 會壓縮從 “1 ”位置移動到 “2 ”位置的文件,因此在上例中,會有 auth.log.2.gz 和 auth.log.3.gz。在其他發行版中,logrotate 會用日期尾碼重命名日誌文件,例如 -20200529。這種方案的一個優點是更容易找到特定時間的日誌文件。
你可能想知道,如果 logrotate 在另一個實用程式(如 rsyslogd)想要添加到日誌文件的同時執行旋轉,會發生什麼情況。例如,日誌程式打開日誌文件進行寫入,但在 logrotate 執行重命名之前沒有關閉。在這種有點不尋常的情況下,日誌信息會被成功寫入,因為在 Linux 中,文件一旦打開,I/O 系統就無法知道它被重命名了。但要註意的是,信息出現在的文件將是帶有新名稱的文件,如 auth.log.1。
如果 logrotate 在日誌程式嘗試打開文件之前已經重命名了文件,則 open() 系統調用會創建一個新的日誌文件(如 auth.log),就像 logrotate 未運行時一樣。
7.1.4 日誌維護
存儲在 /var/log/journal 中的日誌不需要輪換,因為 journald 本身可以識別並刪除舊信息。與傳統的日誌管理不同,journald 通常根據日誌文件系統的剩餘空間、日誌占文件系統的百分比以及日誌的最大大小來決定是否刪除信息。日誌管理還有其他選項,如日誌信息的最大允許年齡。有關預設設置和其他設置的說明,請參閱 journald.conf(5) 手冊。
7.1.5 深入瞭解系統日誌
在瞭解了 syslog 和日誌的部分操作細節之後,現在是時候退一步,看看日誌為何以及如何以這種方式運行。本討論更偏重理論而非實踐;您可以輕鬆跳到書中的下一個主題。
20 世紀 80 年代,一個缺口開始出現: Unix 伺服器需要一種記錄診斷信息的方法,但當時還沒有這樣做的標準。當 syslog 與 sendmail 電子郵件伺服器一起出現時,其他服務的開發者都很樂意採用它。RFC 3164 描述了 syslog 的演變過程。
其機制相當簡單。傳統的 syslogd 監聽並等待 Unix 域套接字 /dev/log 上的信息。syslogd 的另一個強大功能是,除了 /dev/log 之外,它還能監聽網路套接字,使客戶端機器能通過網路發送消息。
這樣就可以將整個網路的所有 syslog 消息整合到一個日誌伺服器上,因此 syslog 深受網路管理員的歡迎。許多網路設備(如路由器和嵌入式設備)都可以充當 syslog 客戶端,向伺服器發送診斷信息。
Syslog 具有經典的客戶端-伺服器架構,包括自己的協議(目前在 RFC 5424 中定義)。不過,該協議並不總是標準的,早期的版本除了一些基本的結構外,並沒有太多其他的結構。使用 syslog 的程式員需要為自己的應用程式設計一種描述性的、清晰簡短的日誌信息格式。隨著時間的推移,該協議在增加新功能的同時,也儘可能保持向後相容性。
- 設施、嚴重性和其他欄位
由於 syslog 會將不同類型的信息從不同的服務發送到不同的目的地,因此它需要一種方法來對每條信息進行分類。傳統的方法是使用通常(但不總是)包含在報文中的設施和嚴重性編碼值。除了文件輸出外,即使是非常老的 syslogd 版本,也能根據消息的設施和嚴重性將重要消息發送到控制台或直接發送給特定的登錄用戶--這是早期的系統監控工具。
設施是一種通用的服務類別,用於識別發送消息的對象。設施包括服務和系統組件,如內核、郵件系統和印表機。
嚴重性是日誌信息的緊急程度。共有八個級別,從 0 到 7。它們通常用名稱來表示,不過名稱並不一致,在不同的實現中也有不同:
0: emerg | 4: warning |
---|---|
1: alert | 5: notice |
2: crit | 6: info |
3: err | 7: debug |
設施和嚴重性共同構成優先順序,在系統日誌協議中打包為一個數字。您可以在 RFC 5424 中閱讀有關這些欄位的所有信息,在 syslog(3) 手冊頁中瞭解如何在應用程式中指定它們,在 rsyslog.conf(5) 手冊頁中瞭解如何匹配它們。不過,在 journald 世界中,嚴重性被稱為優先順序(例如,當運行 journalctl -o json 以獲取機器可讀的日誌輸出時),將它們轉換為 journald 時可能會遇到一些困惑。
不幸的是,當你開始研究協議中優先順序部分的細節時,你會發現它跟不上操作系統其他部分的變化和要求。嚴重性定義仍然保持良好,但可用的設施都是硬連接的,包括 UUCP 等很少使用的服務,而且無法定義新的服務(只有一些通用的 local0 至 local7 插槽)。
我們已經討論過日誌數據中的一些其他欄位,但 RFC 5424 還包括結構化數據的規定,即應用程式程式員可用於定義自己欄位的任意鍵值對集。雖然這些數據可以通過一些額外工作與 journald 配合使用,但將它們發送到其他類型的資料庫更為常見。
- Syslog 與 journald 的關係
journald 在某些系統上已經完全取代了 syslog,你可能會問為什麼其他系統上還保留著 syslog。主要有兩個原因:Syslog 有一個定義明確的方法來彙總多台機器上的日誌。如果日誌只在一臺機器上,監控日誌就容易得多;syslog 的版本(如 rsyslogd)是模塊化的,能夠輸出到許多不同的格式和資料庫(包括日誌格式)。這使它們更容易連接到分析和監控工具。
相比之下,journald 強調將單台機器的日誌輸出收集整理成單一格式。
當你想做更複雜的事情時,journald 將日誌輸入不同日誌記錄器的功能提供了高度的通用性。尤其是當你考慮到 systemd 可以收集伺服器單元的輸出並將其發送到 journald 時,你可以訪問比應用程式發送到 syslog 更多的日誌數據。
- 關於日誌記錄的最後說明
Linux 系統上的日誌記錄在其發展歷程中發生了巨大變化,而且幾乎可以肯定的是,它還將繼續發展。目前,在單台機器上收集、存儲和檢索日誌的過程已經明確定義,但日誌記錄的其他方面還沒有標準化。
首先,當你想在機器網路上彙總和存儲日誌時,會有一系列令人眼花繚亂的選擇。集中式日誌伺服器不再是簡單地將日誌存儲在文本文件中,現在可以將日誌存入資料庫,而集中式伺服器本身通常也會被互聯網服務所取代。
其次,日誌的使用性質也發生了變化。曾幾何時,日誌並不被視為 “真實 ”數據;日誌的主要用途是(人工)管理員在出錯時可以讀取的資源。然而,隨著應用程式變得越來越複雜,日誌記錄的需求也隨之增長。這些新需求包括搜索、提取、顯示和分析日誌中數據的能力。雖然我們有很多方法可以在資料庫中存儲日誌,但在應用程式中使用日誌的工具仍處於起步階段。
最後,還要確保日誌的可信度。最初的 syslog 沒有任何認證可言;你只需相信發送日誌的應用程式和/或機器說的是實話。此外,日誌沒有加密,容易被網路窺探。對於安全性要求較高的網路來說,這是一個嚴重的風險。當代的系統日誌伺服器擁有加密日誌信息和驗證日誌來源機器的標準方法。然而,當你深入到單個應用程式時,情況就不那麼明朗了。例如,如何確定自稱是網路伺服器的東西實際上就是網路伺服器?
我們將在本章稍後部分探討一些高級身份驗證主題。現在,我們先來瞭解一下系統中配置文件的基本組織結構。
7.2 /etc 的結構
Linux 系統上的大多數系統配置文件都在 /etc 中。歷史上,每個程式或系統服務都有一個或多個配置文件,由於 Unix 系統中的組件數量龐大,/etc 文件很快就會堆積起來。
這種方法有兩個問題:很難在運行中的系統上找到特定的配置文件,而且很難維護這樣配置的系統。例如,如果你想更改 sudo 配置,就必須編輯 /etc/sudoers。但在更改之後,發行版的升級可能會覆蓋 /etc 中的所有內容,從而抹掉你的自定義配置。
多年來的趨勢是將系統配置文件放在 /etc 下的子目錄中,正如你已經看到的 systemd,它使用 /etc/systemd。雖然 /etc 中仍有一些單獨的配置文件,但如果運行 ls -F /etc,就會發現其中的大部分項目現在都是子目錄。
為瞭解決覆蓋配置文件的問題,你現在可以將自定義配置放在配置子目錄中的單獨文件中,例如 /etc/grub.d 中的文件。
/etc 中有哪些配置文件?基本原則是,單台機器的自定義配置,如用戶信息(/etc/passwd)和網路詳細信息(/etc/network),應放入 /etc。不過,一般的應用細節,如發行版的用戶界面預設設置,則不屬於 /etc。不需要定製的系統預設配置文件通常也放在其他地方,比如 /usr/lib/systemd 中的預打包 systemd 單元文件。
我們已經瞭解了一些與啟動相關的配置文件。下麵我們繼續來看看如何在系統中配置用戶。
7.3 用戶管理文件
Unix 系統允許多個獨立用戶。在內核層,用戶只是一個數字(用戶 ID),但由於記住一個名字比記住一個數字要容易得多,所以在管理 Linux 時,通常使用用戶名(或登錄名)。用戶名只存在於用戶空間,因此任何使用用戶名的程式在與內核對話時,都需要找到其對應的用戶 ID。
7.3.1 /etc/passwd文件
明文文件 /etc/passwd 將用戶名映射到用戶 ID。它看起來像清單 7-1。
root:x:0:0:Superuser:/root:/bin/sh
daemon:*:1:1:daemon:/usr/sbin:/bin/sh
bin:*:2:2:bin:/bin:/bin/sh
sys:*:3:3:sys:/dev:/bin/sh
nobody:*:65534:65534:nobody:/home:/bin/false
juser:x:3119:1000:J. Random User:/home/juser:/bin/bash
beazley:x:143:1000:David Beazley:/home/beazley:/bin/bash
每一行代表一個用戶,有七個欄位,以冒號分隔。第一行是用戶名。接下來是用戶的加密密碼,或者至少是曾經的密碼欄位。在大多數 Linux 系統中,密碼實際上不再存儲在 passwd 文件中,而是存儲在影子文件中(參見第 7.3.3 節)。影子文件的格式與 passwd 類似,但普通用戶沒有讀取影子文件的許可權。passwd 或 shadow 文件中的第二個欄位是加密密碼,看起來像一堆無法讀取的垃圾,如 d1CVEWiB/oppc。Unix 密碼從不以明文存儲;事實上,該欄位並不是密碼本身,而是密碼的派生詞。在大多數情況下,從該欄位中獲取原始密碼異常困難(假設密碼不容易猜到)。
第二個 passwd 文件欄位中的 x 表示加密密碼存儲在影子文件中(應在系統中進行配置)。星號 (*) 表示用戶無法登錄。
如果該密碼欄位為空白(即在一排中看到兩個冒號,如::),則登錄時不需要密碼。請小心這樣的空白密碼。絕對不能讓用戶在沒有密碼的情況下登錄。
其餘的密碼欄位如下:
- 用戶 ID(UID),即用戶在內核中的代表。可以有兩個用戶 ID 相同的條目,但這樣會讓你感到困惑,也可能會讓你的軟體感到困惑,所以要保持用戶 ID 的唯一性。
- 組 ID (GID),應該是 /etc/group 文件中的編號條目之一。組決定文件許可權,除此之外幾乎沒有其他作用。該組也稱為用戶的主組。
-
- 用戶的真實姓名(通常稱為 GECOS 欄位)。有時會在該欄位中發現逗號,表示房間號和電話號碼。
用戶的主目錄。
- 用戶的真實姓名(通常稱為 GECOS 欄位)。有時會在該欄位中發現逗號,表示房間號和電話號碼。
- 用戶的 shell(用戶運行終端會話時運行的程式)。
/etc/passwd 文件語法相當嚴格,不允許有註釋或空行。
註意:/etc/passwd 中的用戶和相應的主目錄統稱為賬戶。不過,請記住這是用戶空間的約定。通常只需在 passwd 文件中輸入一個條目即可;大多數程式要識別一個賬戶,並不需要主目錄的存在。此外,還有一些方法可以在系統中添加用戶,而不需要明確地把他們寫入 passwd 文件;例如,使用 NIS(網路信息服務)或 LDAP(輕量級目錄訪問協議)從網路伺服器添加用戶曾經很常見。
7.3.2 特殊用戶
你會在 /etc/passwd 中發現一些特殊用戶。超級用戶(root)的 UID 始終為 0,GID 始終為 0,如清單 7-1 所示。某些用戶,如 daemon,沒有登錄許可權。nobody 用戶是一個低許可權用戶;一些進程以 nobody 的身份運行,因為它(通常)不能寫入系統中的任何內容。
不能登錄的用戶稱為偽用戶。雖然他們不能登錄,但系統可以用他們的用戶 ID 啟動進程。偽用戶(如 nobody)通常是出於安全考慮而創建的。
同樣,這些都是用戶空間的約定。這些用戶對內核沒有特殊意義;唯一對內核有特殊意義的用戶 ID 是超級用戶的 0。
7.3.3 /etc/shadow 文件
Linux 系統中的影子口令文件(/etc/shadow)通常包含用戶身份驗證信息,包括與 /etc/passwd 中的用戶相對應的加密口令和口令過期信息。
引入影子文件是為了提供一種更靈活(或許也更安全)的密碼存儲方式。它包括一整套庫和實用程式,其中許多很快就被 PAM(可插拔身份驗證模塊;我們將在第 7.10 節介紹這一高級主題)所取代。PAM 並沒有為 Linux 引入一套全新的文件,而是使用 /etc/shadow,但不使用某些相應的配置文件,如 /etc/login.defs。
7.3.4 操作用戶和密碼
普通用戶使用 passwd 命令和其他一些工具與 /etc/passwd 進行交互。使用 passwd 命令更改密碼。你可以使用 chfn 和 chsh 分別更改真實姓名和 shell(shell 必須列在 /etc/shells 中)。這些都是 suid-root 可執行文件,因為只有超級用戶才能更改 /etc/passwd 文件。
- 以超級用戶身份更改 /etc/passwd
因為 /etc/passwd 只是一個普通的純文本文件,所以從技術上講,超級用戶可以使用任何文本編輯器進行修改。要添加一個用戶,只需添加一行適當的內容,併為該用戶創建一個主目錄即可;要刪除一個用戶,則可以反其道而行之。
不過,像這樣直接編輯 passwd 並不是個好主意。這樣做不僅容易出錯,而且如果同時有其他東西在修改 passwd,還可能出現併發問題。通過終端或圖形用戶界面使用單獨的命令對用戶進行更改要容易得多(也安全得多)。例如,要設置用戶密碼,以超級用戶身份運行 passwd user。使用 adduser 和 userdel 分別添加和刪除用戶。
不過,如果你真的必須直接編輯文件(例如,文件已損壞),請使用 vipw 程式,該程式會在你編輯 /etc/passwd 文件時備份並加鎖,以防萬一。要編輯 /etc/shadow 而不是 /etc/passwd,請使用 vipw -s。(希望你永遠不需要使用這兩種方法)。
7.3.5 使用組
Unix 中的組提供了一種在特定用戶間共用文件的方法。其原理是為特定組設置讀取或寫入許可權,將其他人排除在外。這一功能曾經非常重要,因為許多用戶共用一臺機器或網路,但近年來隨著工作站的共用頻率降低,這一功能的重要性也隨之降低。
/etc/group 文件定義了組 ID(如 /etc/passwd 文件中的組 ID)。清單 7-2 是一個示例。
root:*:0:juser
daemon:*:1:
bin:*:2:
sys:*:3:
adm:*:4:
disk:*:6:juser,beazley
nogroup:*:65534:
user:*:1000:
清單 7-2: /etc/group 文件示例
與 /etc/passwd 文件一樣,/etc/group 文件中的每一行都是一組用冒號分隔的欄位。每個條目中的欄位從左到右排列如下:
- 組名 在運行 ls -l 等命令時顯示。
- 組密碼 Unix 組密碼幾乎不使用,也不應該使用(在大多數情況下,sudo 是一個不錯的選擇)。使用 * 或其他預設值。這裡的 x 表示 /etc/gshadow 中有相應的條目,而且幾乎總是禁用密碼,用 * 或 ! 表示。
- 組 ID(一個數字) GID 在組文件中必須是唯一的。這個數字會出現在用戶 /etc/passwd 條目中的用戶組欄位中。
- 屬於組的用戶列表(可選) 除了此處列出的用戶外,在其密碼文件條目中具有相應組 ID 的用戶也屬於該組。
要查看所屬組,請運行 groups。
註意:Linux 發行版通常會為每個新添加的用戶創建一個新的組,組名與用戶名相同。
7.4 getty和登錄
getty 程式會連接到終端並顯示登錄提示。在大多數 Linux 系統中,getty 並不複雜,因為系統只用它在虛擬終端上登錄。在進程列表中,它通常看起來像這樣(例如,在 /dev/tty1 上運行時):
$ ps ao args | grep getty
/sbin/agetty -o -p -- \u --noclear tty1 linux
在許多系統上,你甚至可能看不到 getty 進程,直到你用 CTRL-ALT-F1 等命令訪問虛擬終端。本例顯示的是 agetty,許多 Linux 發行版都預設包含該版本。
輸入登錄名後,getty 會自動替換為登錄程式,並要求輸入密碼。如果你輸入了正確的密碼,login 會(使用 exec())替換為你的 shell。否則,你將收到一條 “登錄不正確 ”的信息。登錄程式的大部分真正身份驗證工作由 PAM 處理(參見第 7.10 節)。
註意:在研究 getty 時,你可能會遇到 “38400 ”這樣的波特率。這種設置已經過時。虛擬終端會忽略波特率;波特率只用於連接真實串列線路。
你現在知道 getty 和 login 的作用了,但你可能永遠不需要配置或更改它們。事實上,你甚至很少會用到它們,因為現在大多數用戶都通過圖形界面(如 gdm)或 SSH 遠程登錄,而這兩種方式都不使用 getty 或 login。
7.5 設置時間
Unix 機器依賴於精確的計時。內核維護著系統時鐘,當您運行 date 等命令時,系統時鐘就會顯示。您也可以使用 date 命令來設置系統時鐘,但這樣做通常不是個好主意,因為您永遠無法精確地掌握時間。系統時鐘應儘可能接近正確時間。
PC 硬體有一個電池支持的實時時鐘(RTC)。RTC 並不是世界上最好的時鐘,但有總比沒有好。內核通常會在啟動時根據 RTC 設置時間,你可以使用 hwclock 將系統時鐘重置為當前的硬體時間。請將硬體時鐘設置為世界協調時(UTC),以避免時區或夏令時校正帶來的麻煩。您可以使用此命令將 RTC 設置為內核的 UTC 時鐘:
# hwclock --systohc --utc
不幸的是,內核在計時方面比 RTC 更差,而且由於 Unix 機器在一次啟動後往往要運行數月或數年,因此容易產生時間漂移。時間漂移是內核時間與真實時間(由原子鐘或其他非常精確的時鐘定義)之間的當前差值。
你不應該試圖用 hwclock 來修複時間漂移,因為基於時間的系統事件可能會丟失或混淆。您可以運行類似 adjtimex 的實用程式,根據 RTC 平滑更新時鐘,但通常最好使用網路時間守護進程(參見第 7.5.2 節)來保持系統時間的正確性。
7.5.1 內核時間表示和時區
內核的系統時鐘將當前時間表示為自世界協調時 1970 年 1 月 1 日午夜 12:00 起的秒數。要查看當前時間,請運行
$ date +%s
為了將這一數字轉換成人類可以讀取的時間,用戶空間程式會將其轉換為本地時間,並對夏令時和其他奇怪的情況(例如居住在印第安納州)進行補償。本地時區由 /etc/localtime 文件控制。(別費勁去看它,這是一個二進位文件)。
系統中的時區文件位於 /usr/share/zoneinfo。你會發現該目錄包含大量時區和時區別名。要手動設置系統的時區,要麼將 /usr/share/zoneinfo 中的文件複製到 /etc/localtime(或建立符號鏈接),要麼使用發行版的時區工具進行更改。命令行程式 tzselect 可以幫助你識別時區文件。
要在一次 shell 會話中使用系統預設時區以外的時區,可將 TZ 環境變數設置為 /usr/share/zoneinfo 中的文件名,然後測試更改,如下所示:
$ export TZ=US/Central
$ date
與其他環境變數一樣,你也可以在單個命令的持續時間內設置時區,如下所示:
$ TZ=US/Central date
7.5.2 網路時間
如果機器與互聯網永久連接,則可以運行網路時間協議 (NTP) 守護進程,通過遠程伺服器來維護時間。這曾由 ntpd 守護進程處理,但與許多其他服務一樣,systemd 已用自己的軟體包(名為 timesyncd)取代了它。大多數 Linux 發行版都包含 timesyncd,而且預設情況下已啟用。你應該不需要對它進行配置,但如果你對如何配置感興趣,timesyncd.conf(5) 手冊頁可以幫到你。最常見的覆蓋是更改遠程時間伺服器。
如果想運行 ntpd,則需要禁用已安裝的 timesyncd。請訪問 https://www.ntppool.org/ 查看相關說明。如果你還想在不同的伺服器上使用 timesyncd,這個網站也可能有用。
如果你的機器沒有永久的互聯網連接,可以使用 chronyd 等守護進程在斷開連接時保持時間不變。
你也可以根據網路時間設置硬體時鐘,以幫助系統在重啟時保持時間一致性。許多發行版都會自動這樣做,但要手動設置,請確保系統時間是通過網路設置的,然後運行以下命令:
# hwclock --systohc –-utc
7.6 使用cron和定時單位調度重覆任務
有兩種方法可以按重覆計劃運行程式:cron 和 systemd 定時器單元。這種能力對於自動執行系統維護任務至關重要。其中一個例子就是日誌文件輪換實用程式,它可以確保硬碟不被舊日誌文件填滿(本章前面已經討論過)。cron 服務一直以來都是實現這一目標的事實標準,我們將詳細介紹它。不過,systemd 的定時器單元在某些情況下可以替代 cron,因此我們也將介紹如何使用它們。
你可以使用 cron 在任何適合你的時間運行任何程式。通過 cron 運行的程式稱為 cron 作業。要安裝 cron 作業,通常需要運行 crontab 命令,在 crontab 文件中創建一個入口行。例如,以下 crontab 文件條目將 /home/juser/bin/spmake 命令安排在每天上午 9:15(當地時區)運行:
15 09 * * * /home/juser/bin/spmake
該行開頭的五個欄位以空格分隔,指定了計劃時間(另見圖 7-3)。欄位依次如下:
- 分鐘(0 至 59)。此 cron 作業設置為第 15 分鐘。
- 小時(0 至 23)。該任務設置為第 9 個小時。
- 月(1 至 31)。
- 月(1 到 12)。
- 星期(0 至 7)。數字 0 和 7 表示星期日。
任何欄位中的星號 (*) 都表示匹配每個值。前面的示例每天都運行 spmake,因為月日、月和星期欄位都填滿了星,cron 將其理解為 “每月的每天、每周的每天都運行此任務”。
如果只在每月的第 14 天運行 spmake,可以使用以下 crontab 行:
15 09 14 * * /home/juser/bin/spmake
您可以為每個欄位選擇多個時間。例如,要在每月的第 5 天和第 14 天運行程式,可以在第三個欄位中輸入 5,14:
15 09 5,14 * * /home/juser/bin/spmake
註意:如果 cron 作業產生標準輸出、錯誤或異常退出,cron 應將此信息通過電子郵件發送給 cron 作業的所有者(假設電子郵件在系統中正常工作)。如果覺得電子郵件令人討厭,可將輸出重定向到 /dev/null 或其他日誌文件。
crontab(5) 手冊頁面提供了有關 crontab 格式的完整信息。
7.6.1 安裝 Crontab 文件
每個用戶都可以擁有自己的 crontab 文件,這意味著每個系統都可能有多個 crontab,通常位於 /var/spool/cron/crontabs。普通用戶不能寫入該目錄;crontab 命令用於安裝、列出、編輯和刪除用戶的 crontab。
安裝 crontab 的最簡單方法是將 crontab 條目放入一個文件,然後使用 crontab 文件將文件安裝為當前的 crontab。crontab 命令會檢查文件格式,確保沒有任何錯誤。要列出 cron 作業,請運行 crontab -l。要刪除 crontab,請使用 crontab -r。
創建初始 crontab 後,使用臨時文件進行進一步編輯可能會有點亂。相反,您可以使用 crontab -e 命令一步完成 crontab 的編輯和安裝。如果您犯了錯誤,crontab 會告訴您錯誤所在,並詢問您是否要重新編輯。
7.6.2 系統 Crontab 文件
許多常見的 cron 激活的系統任務都是以超級用戶身份運行的。不過,Linux 發行版通常會為整個系統設置一個 /etc/crontab 文件,而不是編輯和維護超級用戶的 crontab 來安排這些任務。你不會用 crontab 來編輯這個文件,而且在任何情況下,它的格式都略有不同:在要運行的命令之前,有一個額外的欄位指定了應該運行作業的用戶。(這讓你有機會將系統任務分組,即使它們並非都由同一用戶運行)。例如,在 /etc/crontab 中定義的 cron 作業以超級用戶(root 1)身份在早上 6:42 運行:
42 6 * * * root1 /usr/local/bin/cleansystem > /dev/null 2>&1
註意:某些發行版會在 /etc/cron.d 目錄中存儲額外的系統 crontab 文件。這些文件可以有任何名稱,但格式與 /etc/crontab 相同。可能還有一些目錄,如 /etc/cron.daily,但這裡的文件通常是由 /etc/crontab 或 /etc/cron.d 中特定 cron 作業運行的腳本。
7.6.3 定時器單元
為定期任務創建 cron 作業的另一種方法是創建 systemd 定時器單元。對於一個全新的任務,必須創建兩個單元:一個計時器單元和一個服務單元。之所以要創建兩個單元,是因為定時器單元並不包含要執行任務的任何細節;它只是運行服務單元(或概念上的另一種單元,但最常用的是服務單元)的激活機制。
讓我們從定時器單元開始,看看一對典型的定時器/服務單元。我們將其稱為 loggertest.timer;與其他自定義單元文件一樣,我們將其放在/etc/systemd/system 中(見清單 7-3)。
[Unit]
Description=Example timer unit
[Timer]
OnCalendar=*-*-* *:00,20,40
Unit=loggertest.service
[Install]
WantedBy=timers.target
此計時器每 20 分鐘運行一次,OnCalendar 選項與 cron 語法相似。在此示例中,它在每小時的頂部以及每小時過去 20 分鐘和 40 分鐘時運行。
OnCalendar 的時間格式為年-月-日-時:分:秒。秒欄位是可選的。與 cron 一樣,* 表示通配符,逗號允許多個值。周期/語法也有效;在前面的示例中,可以將 *:00,20,40 改為 *:00/20(每 20 分鐘),以達到同樣的效果。
註意OnCalendar 欄位中的時間語法有很多快捷方式和變化。有關完整列表,請參閱 systemd.time(7) 手冊頁面的日曆事件部分。
相關服務單元名為 loggertest.service(見清單 7-4)。我們在定時器中使用 Unit 選項明確命名了它,但這並非嚴格必要,因為 systemd 會查找與定時器單元文件基本名稱相同的 .service 文件。該服務單元也放在 /etc/systemd/system 中,與第 6 章中的服務單元非常相似。
[Unit]
Description=Example Test Service
[Service]
Type=oneshot
ExecStart=/usr/bin/logger -p local3.debug I\'m a logger
其中最重要的是 ExecStart 這一行,它是服務激活時運行的命令。本例將向系統日誌發送一條信息。
請註意,服務類型使用了 oneshot,這表明服務將運行並退出,在 ExecStart 指定的命令完成之前,systemd 不會認為服務已啟動。這對定時器有一些好處:
你可以在單元文件中指定多個 ExecStart 命令。我們在第 6 章中看到的其他服務單元樣式不允許這樣做。
使用 Wants 和 Before 依賴關係指令激活其他單元時,更容易控制嚴格的依賴關係順序。
您可以在日誌中更好地記錄單元的開始和結束時間。
在本單元示例中,我們使用日誌記錄器向系統日誌和日誌發送條目。在第 7.1.2 節中,您可以按單元查看日誌信息。不過,單元可能會在 journald 接收到消息之前結束。這是一個競賽條件,如果單元完成得太快,journald 將無法查找與 syslog 消息相關的單元(通過進程 ID 查找)。因此,寫入日誌的信息可能不包含單元欄位,導致 journalctl -f -u loggertest.service 等過濾命令無法顯示系統日誌信息。這在運行時間較長的服務中通常不是問題。
7.6.4 cron與定時器單位
cron 實用程式是 Linux 系統中最古老的組件之一;它已經存在了幾十年(比 Linux 本身還要早),其配置格式多年來一直沒有太大變化。當一個東西變得如此老舊時,它就成了被替換的對象。
你剛纔看到的 systemd 定時器單元似乎是一個合乎邏輯的替代品,事實上,許多發行版現在已經將系統級的定期維護任務轉移到了定時器單元上。但事實證明,cron 也有一些優勢:
- 配置更簡單
- 與許多第三方服務相容
- 用戶更容易安裝自己的任務
定時器單元具有以下優勢
- 可通過 cgroups 跟蹤與任務/單元相關的進程
- 可在日誌中跟蹤診斷信息
- 激活時間和頻率的額外選項
- 可使用 systemd 依賴關係和激活機制
也許有一天,cron 作業也能像掛載單元和 /etc/fstab 一樣,擁有一個相容層。不過,僅配置這一點就足以說明 cron 格式不可能在短期內消失。正如你將在下一節看到的那樣,一個名為 systemd-run 的實用程式的確可以在不創建單元文件的情況下創建定時器單元和相關服務,但其管理和實現方式的差異足以讓許多用戶傾向於使用 cron。我們將在下一節中討論這一點。
7.7 使用 at 調度一次性任務
要在未來運行一次作業而不使用 cron,可使用 at 服務。例如,要在晚上 10:30 運行 myjob,請輸入以下命令:
$ at 22:30
at> myjob
用 CTRL-D 結束輸入。(at 工具從標準輸入中讀取命令)。
要檢查作業是否已調度,請使用 atq。要刪除作業,請使用 atrm。還可以通過添加 DD.MM.YY 格式的日期來安排未來幾天的工作,例如,22:30 30.09.15。
at 命令的其他功能不多。雖然它並不常用,但在需要時卻非常有用。
7.7.1 定時器單位等價物
你可以使用 systemd 定時器單位來替代 at。這些定時器單元比前面提到的周期性定時器單元更容易創建,可以像下麵這樣在命令行上運行:
# systemd-run --on-calendar='2022-08-14 18:00' /bin/echo this is a test
Running timer as unit: run-rbd000cc6ee6f45b69cb87ca0839c12de.timer
Will run service as unit: run-rbd000cc6ee6f45b69cb87ca0839c12de.service
systemd-run 命令會創建一個瞬時定時器單元,你可以使用常用的 systemctl list-timers 命令查看該單元。如果不關心具體時間,可以使用 --on-active 指定一個時間偏移(例如,--on-active=30m 表示未來 30 分鐘)。
註意使用 --on-on-calendar 時,除了時間外,還必須包含(未來)日曆日期。否則,定時器和服務單元將保持不變,定時器每天在指定時間運行服務,就像前面描述的創建普通定時器單元一樣。該選項的語法與定時器單元中的 OnCalendar 選項相同。
7.8 作為普通用戶運行的定時器單元
到目前為止,我們看到的所有 systemd 定時器單元都是以 root 用戶身份運行的。以普通用戶身份創建定時器單元也是可行的。為此,請在 systemd-run 中添加 --user 選項。
不過,如果在單元運行前註銷,單元將無法啟動;如果在單元完成前註銷,單元將終止。出現這種情況是因為 systemd 有一個與登錄用戶相關聯的用戶管理器,而這是運行定時器單元所必需的。您可以使用以下命令讓 systemd 在您註銷後保留用戶管理器:
$ loginctl enable-linger
以 root 用戶身份,也可以為其他用戶啟用管理器:
# loginctl enable-linger user
7.9 用戶訪問主題
本章餘下部分將介紹用戶如何獲得登錄許可權、切換到其他用戶以及執行其他相關任務。這部分內容有些高深,如果你已經準備好接觸一些進程內部知識,歡迎跳到下一章。
7.9.1 用戶 ID 和用戶切換
我們已經討論過 sudo 和 su 等 setuid 程式如何允許你臨時更改用戶,也介紹過登錄等控制用戶訪問的系統組件。也許你想知道這些組件是如何工作的,以及內核在用戶切換中扮演了什麼角色。
當你臨時切換到另一個用戶時,你所做的其實就是改變你的用戶 ID。有兩種方法可以做到這一點,內核都會處理。第一種是使用 setuid 可執行文件,這在第 2.17 節中已有介紹。第二種是通過 setuid() 系列系統調用。該系統調用有幾個不同的版本,以適應與進程相關的各種用戶 ID,這將在第 7.9.2 節中介紹。
內核有關於進程能做什麼或不能做什麼的基本規則,但以下三個要點涵蓋了 setuid 可執行文件和 setuid():
- 只要有足夠的文件許可權,進程就可以運行 setuid 可執行文件。
- 以根用戶(用戶 ID 0)身份運行的進程可以使用 setuid() 變成任何其他用戶。
- 以非 root 用戶身份運行的進程在如何使用 setuid() 方面受到嚴格限制;在大多數情況下,它不能使用 setuid()。
由於存在這些規則,如果要將用戶 ID 從普通用戶切換到其他用戶,通常需要結合使用多種方法。例如,sudo 可執行文件是 setuid root,一旦運行,它就可以調用 setuid() 成為另一個用戶。
註意:用戶切換的核心與密碼或用戶名無關。正如你在第 7.3.1 節的 /etc/passwd 文件中第一次看到的那樣,這些都是嚴格意義上的用戶空間概念。你將在第 7.9.4 節中瞭解更多有關其工作原理的細節。
7.9.2 進程所有權、有效 UID、真實 UID 和保存 UID
到目前為止,我們對用戶 ID 的討論已經簡化。實際上,每個進程都有一個以上的用戶 ID。到目前為止,大家已經熟悉了有效用戶 ID(有效 UID 或 euid),它定義了進程的訪問許可權(最重要的是文件許可權)。第二個用戶 ID,即真實用戶 ID(real UID,或 ruid),表示進程由誰啟動。通常情況下,這些ID是相同的,但當你運行一個setuid程式時,Linux會在執行過程中將euid設置為程式的所有者,但會在ruid中保留你原來的用戶ID。
有效用戶 ID 和真實用戶 ID 之間的區別很容易混淆,以至於很多關於進程所有權的文檔都是錯誤的。
把 euid 看作行為者,把 ruid 看作所有者。ruid 定義了可以與運行中的進程交互的用戶--最重要的是,哪個用戶可以殺死進程並向其發送信號。例如,如果用戶 A 啟動了一個新進程,該進程以用戶 B 的身份運行(基於 setuid 許可權),那麼用戶 A 仍然擁有該進程,並可以殺死它。
我們已經看到,大多數進程都有相同的 euid 和 ruid。因此,ps 和其他系統診斷程式的預設輸出只顯示 euid。要查看系統中的兩個用戶 ID,請試試下麵的方法,但如果發現系統中所有進程的兩個用戶 ID 列都相同,也不要驚訝:
$ ps -eo pid,euser,ruser,comm
要想製造一個異常,以便在列中看到不同的值,可以嘗試創建一個 sleep 命令的 setuid 副本,運行該副本幾秒鐘,然後在副本終止前在另一個視窗中運行前面的 ps 命令。
除了真實和有效的用戶 ID 外,還有一個保存的用戶 ID(通常不是縮寫),這就更令人困惑了。進程可以在執行過程中將其 euid 切換為 ruid 或保存的用戶 ID。(讓事情變得更複雜的是,Linux 還有另一個用戶 ID:文件系統用戶 ID 或 fsuid,它定義了訪問文件系統的用戶,但很少使用)。
- 典型的 Setuid 程式行為
ruid 的概念可能與你以前的經驗相矛盾。為什麼你不需要經常處理其他用戶 ID 呢?例如,用 sudo 啟動一個進程後,如果要殺死它,仍然要使用 sudo,而不能用自己的普通用戶殺死它。在這種情況下,你的普通用戶不應該是 ruid 嗎?
造成這種行為的原因是,sudo 和許多其他 setuid 程式通過 setuid() 系統調用之一明確更改了 euid 和 ruid。這些程式之所以這樣做,是因為當所有用戶 ID 不匹配時,往往會產生意想不到的副作用和訪問問題。
註意:如果你對用戶 ID 切換的細節和規則感興趣,請閱讀 setuid(2) 手冊頁面,並查看 SEE ALSO 部分列出的其他手冊頁面。有許多不同的系統調用適用於不同的情況。
有些程式不喜歡使用 root 的 ruid。為防止 sudo 更改 ruid,請在 /etc/sudoers 文件中添加此行(小心會對你想以 root 身份運行的其他程式產生副作用!):
Defaults stay_setuid
- 安全問題
由於 Linux 內核通過 setuid 程式和隨後的系統調用來處理所有用戶開關(以及文件訪問許可權),因此系統開發人員和管理員必須對兩件事格外小心:擁有 setuid 許可權的程式的數量和質量;這些程式會做什麼。
如果你複製了一個設置為 root 的 bash shell,那麼任何本地用戶都可以執行它並完全運行系統。就是這麼簡單。此外,即使是設置為 “root ”用戶的特殊用途程式,如果存在漏洞,也會帶來危險。利用以 root 身份運行的程式中的弱點是入侵系統的主要方法,此類漏洞數不勝數。
由於入侵系統的方法如此之多,因此防止入侵是一件多方面的事情。防止不必要的活動進入系統的最基本方法之一是使用用戶名和密碼進行用戶身份驗證。
7.9.3 用戶識別、身份驗證和授權
多用戶系統必須在三個方面為用戶安全提供基本支持:識別、驗證和授權。安全的識別部分回答了用戶是誰的問題。身份驗證部分要求用戶證明自己的身份。最後,授權用於定義和限制用戶的許可權。
說到用戶身份驗證,Linux 內核只知道進程和文件所有權的數字用戶 ID。內核知道如何運行setuid可執行文件的授權規則,以及用戶ID如何運行setuid()系列系統調用來從一個用戶變為另一個用戶。但是,內核對身份驗證(用戶名、密碼等)一無所知。實際上,與身份驗證有關的一切都發生在用戶空間。
我們在第 7.3.1 節中討論了用戶 ID 和密碼之間的映射,現在我們將討論用戶進程如何訪問這一映射。我們將從一個過於簡單的情況開始,即用戶進程想知道自己的用戶名(與 euid 對應的名稱)。在傳統的 Unix 系統中,進程可以這樣獲取用戶名:
- 進程使用 geteuid() 系統調用向內核詢問其 euid。
- 進程打開 /etc/passwd 文件並從頭開始讀取。
- 進程讀取 /etc/passwd 文件中的一行。如果沒有可讀取的內容,則說明進程未能找到用戶名。
- 進程會將這一行解析為多個欄位(將冒號之間的所有內容分隔開來)。第三個欄位是當前行的用戶 ID。
- 進程會將步驟 4 中的 ID 與步驟 1 中的 ID 進行比較。如果兩者相同,則步驟 4 中的第一個欄位就是所需的用戶名,進程可以停止搜索並使用該名稱。
- 進程將進入 /etc/passwd 中的下一行,並返回第 3 步。
這是一個很長的過程,實際執行起來通常更加複雜。
7.9.4 使用庫獲取用戶信息
如果每個需要知道當前用戶名的開發人員都必須編寫剛纔所看到的所有代碼,那麼系統就會變得雜亂無章、漏洞百出、臃腫不堪、無法維護。幸運的是,我們通常可以使用標準庫來執行類似的重覆性任務;在這種情況下,通常只需在得到 geteuid() 的答案後調用標準庫中的 getpwuid() 函數即可獲得用戶名。(有關這些調用的工作原理,請參閱相關手冊)。
系統中的可執行文件共用標準庫,因此你可以在不更改任何程式的情況下對身份驗證實現進行重大修改。例如,只需更改系統配置,就可以不再使用 /etc/passwd 訪問用戶,而是使用 LDAP 等網路服務。
這種方法在識別與用戶 ID 相關聯的用戶名時效果很好,但密碼則比較麻煩。第 7.3.1 節介紹了傳統上如何將加密密碼作為 /etc/passwd 文件的一部分,因此如果要驗證用戶輸入的密碼,就需要加密用戶輸入的內容,然後與 /etc/passwd 文件的內容進行比較。
這種傳統的實現方式有很多局限性,包括
- 無法為加密協議設定全系統標準。
- 它假定你可以訪問加密密碼。
- 假定每次用戶要訪問需要身份驗證的內容時,都要提示用戶輸入密碼(這很煩人)。
- 假設你想使用密碼。如果你想使用一次性令牌、智能卡、生物識別或其他形式的用戶身份驗證,就必須自己添加支持。
第 7.3.3 節中討論的影子口令軟體包的開發就受到了其中一些限制,它在允許全系統口令配置方面邁出了第一步。但是,PAM 的設計和實施解決了大部分問題。
7.10 可插拔身份驗證模塊
為了適應用戶身份驗證的靈活性,1995 年,Sun Microsystems 公司提出了一個新的標準,稱為可插拔身份驗證模塊(PAM Pluggable Authentication Modules),這是一個用於身份驗證的共用庫系統(開放軟體基金會 RFC 86.0,1995 年 10 月)。要對用戶進行身份驗證,應用程式會將用戶交給 PAM,以確定用戶是否能成功識別自己。這樣,就可以相對容易地添加對其他身份驗證技術(如雙因素和物理密鑰)的支持。除了支持身份驗證機制,PAM 還為服務提供了有限的授權控制(例如,如果你想拒絕向某些用戶提供 cron 等服務)。
由於存在多種身份驗證情況,PAM 採用了大量可動態載入的身份驗證模塊。每個模塊都執行特定的任務,是一個共用對象,進程可以動態載入併在其可執行空間中運行。例如,pam_unix.so 就是一個可以檢查用戶密碼的模塊。
至少可以說,這是一項棘手的工作。編程界面並不簡單,而且 PAM 是否真的能解決現有的所有問題也不清楚。不過,幾乎所有需要在 Linux 系統上進行身份驗證的程式都支持 PAM,而且大多數發行版都使用 PAM。由於 PAM 是在現有的 Unix 身份驗證 API 的基礎上工作的,因此將支持集成到客戶端幾乎不需要任何額外的工作。
7.10.1 PAM 配置
我們將通過檢查 PAM 的配置來瞭解其基本工作原理。PAM 的應用程式配置文件通常位於 /etc/pam.d 目錄中(舊系統可能只使用一個 /etc/pam.conf 文件)。大多數安裝文件都包含許多文件,因此您可能不知道從哪裡開始。有些文件名(如 cron 和 passwd)與您已經知道的系統部分相對應。
由於不同發行版中這些文件的具體配置差異很大,因此很難找到一個普遍適用的示例。我們來看看 chsh(更改 shell 程式)的配置行示例:
auth requisite pam_shells.so
這一行說明,用戶的 shell 必須列在 /etc/shells 中,這樣用戶才能成功通過 chsh 程式的身份驗證。讓我們看看是如何做到的。每一行配置都有三個欄位:依次是函數類型、控制參數和模塊。下麵是它們在本例中的含義:
- 功能類型 用戶應用程式要求 PAM 執行的功能。這裡是 auth,即驗證用戶身份的任務。
- 控制參數 該設置控制 PAM 在當前行(本例中為必要條件)的操作成功或失敗後的操作。我們很快就會討論這個問題。
- 模塊 該行運行的身份驗證模塊,決定該行的實際操作。在這裡,pam_shells.so 模塊會檢查用戶當前的 shell 是否列在 /etc/shells 中。
- PAM 配置詳見 pam.conf(5) 手冊頁面。讓我們來看看其中的一些要點。
用戶應用程式可以要求 PAM 執行以下四種功能之一:
- auth 驗證用戶(查看用戶身份)。
- account 檢查用戶賬戶狀態(例如,用戶是否被授權做某事)。
- session 僅對用戶當前會話執行某些操作(如顯示每日信息)。
- password 更改用戶密碼或其他憑證。
對於任何配置行,模塊和功能共同決定 PAM 的操作。一個模塊可以有多個功能類型,因此在確定配置行的目的時,請務必將功能和模塊視為一對。例如,pam_unix.so 模塊在執行 auth 功能時會檢查密碼,但在執行 password 功能時會設置密碼。
控制參數和堆疊規則
PAM 的一個重要特性是其配置行指定的規則可以堆疊,這意味著在執行一個功能時可以應用多個規則。這也是控制參數非常重要的原因:一行操作的成功或失敗會影響後續行,或導致整個函數的成功或失敗。
控制參數有兩種:簡單語法和更高級的語法。以下是規則中的三種主要簡單語法控制參數:
sufficient 如果該規則成功,則身份驗證成功,PAM 無需查看其他規則。如果規則失敗,PAM 將繼續查看其他規則。
必要條件 如果該規則成功,PAM 將繼續執行其他規則。如果該規則失敗,則身份驗證不成功,PAM 無需查看更多規則。
required 如果此規則成功,PAM 將轉到其他規則。如果該規則失敗,PAM 會繼續執行其他規則,但無論其他規則的最終結果如何,PAM 都會返回身份驗證不成功。
繼續前面的示例,下麵是 chsh 身份驗證函數的堆棧示例:
auth sufficient pam_rootok.so
auth requisite pam_shells.so
auth sufficient pam_unix.so
auth required pam_deny.so
在此配置下,當 chsh 命令要求 PAM 執行身份驗證功能時,PAM 會執行以下操作(流程圖見圖 7-4):
pam_rootok.so 模塊會檢查 root 是否是試圖進行身份驗證的用戶。如果是,則立即成功,不再嘗試其他身份驗證。這是因為控制參數被設置為 sufficient,這意味著該操作成功後,PAM 會立即向 chsh 報告成功。否則,將繼續執行步驟 2。
pam_shells.so 模塊會檢查 /etc/shells 中是否列出了用戶的 shell。如果沒有,模塊將返回失敗,必要的控制參數表示 PAM 必須立即將此失敗報告給 chsh,並不再嘗試進一步的身份驗證。否則,模塊將返回成功,並執行 requisite 控制標誌;繼續執行第 3 步。
pam_unix.so 模塊會詢問用戶密碼併進行檢查。控制參數設置為 sufficient,因此只要該模塊成功(密碼正確),PAM 就會向 chsh 報告成功。如果密碼不正確,PAM 會繼續執行第 4 步。
pam_deny.so 模塊總是失敗,由於控制參數被設置為 required,PAM 會向 chsh 報告失敗。這是一個預設設置,用於在沒有任何嘗試的情況下。(請註意,必填控制參數並不會導致 PAM 的函數立即失敗--它會運行堆棧中剩餘的任何行,但 PAM 始終會嚮應用程式報