2010年3月17日

[Shell Script] 批次修改使用者的密碼有效期限 - V1

最近公司在做資訊稽核,其中有一塊牽涉到主機上的帳號管理。根據公司的資安規範,每個使用者帳號 (系統內建帳號除外) 必須符合以下限制:
  1. 密碼長度不得小於6個字元
  2. 密碼有效期為90天 (也就是每90天就要變更一次密碼的意思)
  3. 不得使用三代以內的密碼
  4. 密碼不得與帳號相同
除了第四點是系統內建,不需要調整設定以外,其餘幾項都需要調整系統設定 (Solaris 只要修改系統內建的設定檔即可;針對第1和第3項要求,Linux 則必須要使用 PAM 模組的設定來達成,下次再撰文說明),本文先針對第二點進行說明。
要設定「密碼有效期為90天」並不困難,不管在 Solaris / Linux 上都只要透過 passwd 指令或者 chage 指令即可完成,但由於我覺得 chage 指令很容易使用,查詢所得的結果也很容易閱讀,所以這次就以 chage 來實作。
有關 chage 指令的用法,除了參考鳥哥的網站,也可以直接下 man chage 指令來查詢內建的 man page,我用的指令如下:
chage -I 1 M 90 -W 30 [username] chage -d 2010-03-17 [username]
(第二個指令是為了避免設定好第一個指令以後,所有系統上 90 天內不曾修改密碼的使用者下次登入系統時都要重新設定密碼,因此把「最後一次修改密碼」的日期改為今天)
設定完畢之後,再下 chage –l [username] 指令 (英文的 l) 即可查詢該使用者的帳號狀態。
OK, 既然 chage 指令這麼簡單,那本文有甚麼好講的呢?如果只需要在單一主機上對少數幾個帳號作如此的設定,當然是直接下幾個指令就搞定,問題是這次需要修改的主機超過 50 台,每台主機上面又有一大堆帳號,真要這樣下指令的話會瘋掉!(而且還有第1和第3項要求要作到,那又是另外兩行 Perl 程式) 所以我就把這些機械化的動作寫成一個 script 啦!
第一步:詢問使用者 chage -d 要使用的日期參數:
echo “set Last password change day (YYYY-MM-DD): “ read lDays #把使用者輸入的參數存到 $lDays 變數中
接下來判斷使用者輸入的字串是否符合 YYYY-MM-DD 的格式:
syntaxOK=`echo $lDays | perl –ne ‘print “OK” if /^\d{4}-\d{2}-\d{2}/’` (`` 是 backtick,不是單引號哦~)
if [ "$syntaxOK" != "OK" ]; then    echo "Wrong syntax! Usage: YYYY-MM-DD"    exit fi
這裡用 perl 來寫 regular expression 會比用 shell script 簡單很多,shell script 要這樣寫 (因為沒有支援 \d 這樣的表示法,而且也不支援 {4} 的寫法):
syntaxOK=`echo $lDays | grep "^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]$"`
神奇的是用 perl 的話不用加上「$」來限制結尾要是 2 個數字,就自然可以判斷出「1111-11-111」是不符合 pattern 的,但是用 shell script 中的 grep 的話就要 (用 egrep 還是要加上「$」)。
第二步:列出系統中所有的使用者,除了系統內建帳號以外:
我參考的是 Listing all users on the system 一文,但是在 awk 的主程式中加上 regular expression 的判斷:
for name in $(awk 'BEGIN{FS=":"} { if($3 ~ /^5[0-9][0-9]/) {print $1}}' < "$PASSWORD_FILE" )
利用這個 pattern 可以過濾出第三欄 ($3) 的值是「5xx」(xx 為數字) 的資料,且印出第一欄 ($1)。
第三步:過濾 uid > 500 的帳號中,屬於系統帳號 or 給程式使用的帳號:
一般來說 uid < 500 的是保留給系統使用的帳號,但 uid > 500 的帳號中仍有可能存在單純給程式使用的帳號 (e.g., 給 FTP / scp 使用),因此需要進一步過濾。我的作法世新增一個 AccountExcluded.txt 檔案,以一行一個帳號的方式,將欲排除的帳號寫入這個檔案,接下來用一個迴圈就可以比對:
accountExcluded=`cat ./AccountExcluded.txt` excluded="0" for exclude in $accountExcluded do      if [ $name = $exclude ]; then             excluded="1"      fi done
第四步:執行 chage 指令
到了這個步驟才是整個 script 的核心,對於每個不在 AccountExcluded.txt 檔案中的帳號,只要在以上的迴圈中執行以下兩行即可:
chage -M 90 -W 30 $name chage -d $lDays $name
整個 script 其實就只有這樣而已,為了避免更動到系統帳號才額外加了一堆有的沒的程式碼 (為了寫 log 又另外加了一堆),在這次練習的過程中,發現還是寫 RegExp 最好玩 (awk 也很有趣),其他的程式碼都挺無聊的。
這個 script 未來還有很多可以 enhance 的地方,依照重要性由高到低排列:
  1. 在本機上登入所有需要修改的主機,將本機的 shell script 檔&設定檔複製到所有的主機上執行 (不然一台一台的登入也是很累人的)。將這個架構建立起來以後,未來有遇到類似要再每台主機上調整設定的需求,就可按照同樣的模式寫成一個 script 來執行。
  2. 加強 YYYY-MM-DD 的判斷,找找看有沒有轉換成 Date 之類型態的方式可用,順便避免 2/29 的問題。
  3. 把讀取兩次 /etc/passwd 的寫法改成只讀取一次,第一次讀出資料以後存到一個 array 裡面就好。
  4. 加強過濾 /etc/passwd 檔案時使用的 regular expression,雖然目前實務上接觸到的主機還不至於帳號多到 uid > 600 或者更高,但可以當作一個練習 regular expression 的機會。
有興趣參考完整原始碼的朋友請到這裡下載 (V1)。

參考:[2010-12-13] [Shell Script] 批次修改使用者的密碼有效期限 - V2

5 則留言:

ctbstrong 提到...

一、echo “set Last password change day (YYYY-MM-DD): “
read lDays #把使用者輸入的參數存到 $lDays 變數中
=>如果是用 Bourne Again Shell,read 可以用 -p 產生提示字串,Korn Shell,則可以用 read lDays?"set Last password change day (YYYY-MM-DD): "來產生提示字元,Bourne Shell 的 read 比較陽春,就沒辦法了。

二、syntaxOK=`echo $lDays | perl –ne ‘print “OK” if /^\d{4}-\d{2}-\d{2}/’` (`` 是 backtick,不是單引號哦~)
if [ "$syntaxOK" != "OK" ]; then
echo "Wrong syntax! Usage: YYYY-MM-DD"
exit
fi
=>perl 既然有 print 指令,就不必再使用一個變數來判斷輸出了。直接寫成echo $lDays | perl -ne 'print "Wrong syntax! Usage: YYYY-MM-DD" if /^\d{4}-\d{2}-\d{2}/' 就行了。

三、accountExcluded=`cat ./AccountExcluded.txt`
excluded="0"
for exclude in $accountExcluded
do
if [ $name = $exclude ]; then
excluded="1"
fi
done
=>平常我也會這麼用,但是如果 AccountExcluded.txt 資料量過大就會出問題。後來學到用 xargs 這個好用的指令,加參數 -n MAX-ARGS 可以設定一次處理的筆數,-i 可以將值放進 '{}' 做進一步處理(跟 find 指令中的 {} 很類似),加上 -0 則可以在輸出值後面加入 null(find 指令是加參數 -print0),預設是空白。所以上述程式可以改寫成
excluded="0"
cat ./AccountExcluded.txt | xargs -n1 -i sh -c "if [ '{}' = $excluded ]; then excluded="1";fi

熱浪 提到...

感謝Tim的分享
我將AccountManagement_V1.sh以perl改寫,但沒有加上pam的部分
1、日期輸入做基本的判斷,但沒有判斷大小月的問題
2、將/etc/passwd讀入hash(%user)
以下為程式碼:

--||code||--
#!/usr/bin/perl
#modify http://dotnetmis91.blogspot.com/2010/03/shell-script-v1.html
# I changed the language to perl. edit by geneseven.
my ($lastChg_date,$flag);
my (%user, %excluded);

open EXCLUDED,"/tmp/AcctExcluded.txt" || die "Can't open the file.\n";
open PASSWD,"/etc/passwd" || die "Can't open the file.\n";

#Waittin for user input date.
inputDate();
while(chkDate()){
print "The Last password change day is wrong.\n";
inputDate();
}
my $lastChg_date = sprintf "%4d-%02d-%02d",$yr, $mo, $da; #Format the Date.
print "Setting The Last passwd change day is $lastChg_date\n";

#Excluded list make to hash table
while($user=){
chomp $user;
$excluded{$user}=1;
}

#Finding out user who must change passwd
while(){
($user,$passwd,$uid)=split /:/,$_;
$user{$user}=1 if ($uid >500 && !exists $excluded{$user}) ;
}

#Show user want to change passwd
for $user(keys %user){ print "$user\n"};

print "Do you want to execute chage command to the above users? (Y/N)";
chomp($YorN=);
$YorN =~ tr/A-Z/a-z/;
if($YorN =~ /y/){
foreach(keys %user){
qx{chage -M 90 -W 30 $_};
qx{chage -d $lastChg_date $_};
}
}

#check the date.
sub chkDate{
$flag=0;
if($yr < flag="1;"> 12){
print "$mo The month must between 00 and 12\n";
$flag=1;
}
if ($da <> 31){
print "$day The day must between 00 and 31\n";
$flag=1;
}
}
--||code||--

熱浪 提到...

由於Perl的Diamond Operator用在STDIN標準輸出,會被視為HTML標籤而忽略,所以改放在點我下載(Content will be available for download until July 18, 2010 18:27 PDT. )

Unknown 提到...

Hi 熱浪,感謝你的熱心回覆,最近都在熬夜看球賽,不好意思沒有仔細研究你的程式,我會儘快看的 XD

Unknown 提到...

Hi 熱浪,你的 perl 程式我看完囉,又多學到很多東西 :D (用 qx 來執行系統指令比用 backtick 更清楚易讀多了!)

沒想到距離這篇文章 po 完已經過了 3 個多月,一直都沒有把 enhance 的部份做出來 (這程式只用了一次而已 @@")。

後來也想到,如果主機上的使用者帳戶是用 NIS 來集中管理,就不需要管 enhance 1 了 ...

Google Spreadsheet 裡用規則運算式

最近因為工作關係,遇到要用 Google Form 及 Google Sheet 所以研究了 Google Sheet 裡的一些 function 怎麼用 首先,分享一下如何在 Google Sheet 裡用規則運算 :D