我有两个 CentOS 系统,我想(仅)同步存储在/etc/shadow
从系统 A(本地)到系统 B(远程)的密码哈希值,但仅限于 UID > 1000 且存在于两个系统上的用户(基于用户名,而不是 UID(UID 对于 A 和 B 上的同一用户名可能会有所不同)。
我无法使用 rsync 或 LDAP 或 NIS 等解决方案。我也无法触摸这些系统上 UID < 1000 的任何帐户。
由于主机 A 和 B 上的用户 UID 可能不同 - 要将密码哈希从 A 同步到 B,以下内容很重要:(1) 用户名必须存在于两个系统上 (2) 用户名的 UID 必须大于 1000(可以不同) A系统和B系统
我找到了一个很好的 perl 脚本雷诺·邦普伊斯这可能需要对我的要求进行一些调整,它不应该修改/etc/passwd
或/etc/group
。我不是 Perl 程序员,所以我在这里寻求帮助。先感谢您。
#!/usr/bin/perl -w
use Net::SCP qw(scp);
use strict;
use constant TRUE => (1==1);
use constant FALSE => (1==0);
#--------------------------------------------------------
# Configuration
# Modify as needed
#--------------------------------------------------------
my $remoteHost = '10.13.113.2'; # email backup server
my $minUID = 500;
my $maxUID = 30000;
my $minGID = 500;
my $maxGID = 30000;
#--------------------------------------------------------
# Internal variables, normally not to be modified.
#--------------------------------------------------------
my $systemConfigDir = '/etc';
my $tmpDir = $ENV{TMPDIR} || $ENV{TMP} || $ENV{TEMP} || '/tmp';
#--------------------------------------------------------
# Main
#--------------------------------------------------------
# STEP 1
# Get the remote files to /tmp and
# clean them of their normal users
ProcessFiles('remote');
# STEP 2
# Append the local normal users to the temp files
# and then send them back to the remote
ProcessFiles('local');
#--------------------------------------------------------
# ProcessFiles sub does one of two things:
# - if the passed argument is 'remote', then fetch each
# user account file from the remote server, then remove
# all normal users from each file, only keeping the
# system users.
# - if the passed argument is 'local', then appends all
# normal local users to the previously fetched and
# cleaned-up files, then copies them back to the remote.
#--------------------------------------------------------
sub ProcessFiles {
my $which = shift;
my $tmpfile;
my %username = ();
my %usergroup = ();
my %userUID = ();
my %userGID = ();
my @info;
foreach my $f ('passwd','group','shadow','gshadow') {
my $tmpfile = "$tmpDir/$f.REMOTE";
if ($which eq 'remote') {
# Fetch the remote file
unlink $tmpfile if -e $tmpfile;
scp("$remoteHost:$systemConfigDir/$f", $tmpfile)
or die ("Could not get '$f' from '$remoteHost'");
}
# Glob the file content
open CONFIGFILE, (($which eq 'remote') ? $tmpfile : "$systemConfigDir/$f");
my @lines = <CONFIGFILE>;
close CONFIGFILE;
# Open the temp file, either truncating it or in append mode
open TMPFILE, (($which eq 'remote') ? ">$tmpfile" : ">>$tmpfile" )
or die "Could not open '$tmpfile' for processing";
foreach my $line (@lines) {
# Skip comments, although they should be illegal in these files
next if $f =~ /^\s*#/;
@info = (split ':', $line);
if ($f eq 'passwd') {
my $uid = $info[2];
my $isnormaluser = ($uid > $minUID) && ($uid < $maxUID);
next if (($which eq 'remote') ? $isnormaluser : !$isnormaluser);
$username{$info[0]} = TRUE;
$userUID{$uid} = TRUE;
$userGID{$info[3]} = TRUE;
} elsif ($f eq 'group') {
my $gid = $info[2];
my $isnormalgroup = ($gid > $minGID) && ($gid < $maxGID);
next if (($which eq 'remote') ? $isnormalgroup : !$isnormalgroup);
$usergroup{$info[0]} = TRUE;
} elsif ($f eq 'shadow') {
next if !exists $username{$info[0]};
} else {
next if !exists $usergroup{$info[0]};
}
# Any line that reaches this point is valid
print TMPFILE $line;
}
close TMPFILE;
if ($which eq 'local') {
# send the file back
scp($tmpfile, "$remoteHost:$systemConfigDir/$f") or
die ("Could not send '$f' to '$remoteHost'");
unlink $tmpfile;
}
}
}
#--------------------------------------------------------
# Make sure we cleanup the temp files when we exit
#--------------------------------------------------------
END {
my $tmpfile;
foreach my $f ('passwd','group','shadow','gshadow') {
$tmpfile = "$tmpDir/$f.REMOTE";
unlink $tmpfile if -e $tmpfile;
}
}
答案1
基于@roaiama对该join
命令的使用,这个答案用于getent
获取passwd和shadow文件,而不是直接读取它们,然后chpasswd
在远程主机上使用来更改密码。
由于 ,密码更改代码更简单chpasswd
,但制作旧影子条目的备份副本有点复杂,因为我们getent shadow
也在远程主机上使用。
join -t : -j 1 -o 2.{1..2} \
<(getent passwd | awk -F: '$3 > 1000 {print $1}' | sort) \
<(getent shadow | sort) |
ssh remotehost 'umask 0027 &&
getent shadow > /etc/shadow.old &&
chgrp shadow /etc/shadow.old &&
chpasswd -e 2>/dev/null'
它仅将前两个字段,即用户名和加密密码(输出格式是每行一个“用户名:密码”对)传输到 ssh 中。制作旧影子文件的备份副本后,远程 shell 运行chpasswd
以更改标准输入上指定的密码。
该-e
选项表明chpasswd
密码已经加密。如果没有该选项,它将重新加密提供的密码。
chpasswd
将在 stderr 上抱怨远程系统上不存在的任何用户名,但仍会更改确实存在的用户名的密码。 chpasswd
的 stderr 可以重定向到 /dev/null,如我上面所示。
注意:最好将 stderr 通过管道传输到脚本中,该脚本仅删除预期且无害的“用户名不存在”错误,同时仍显示其他错误。在我的测试虚拟机上,不存在的用户输出的错误chpasswd
如下所示:
# printf '%s\n' "foo:bar" "xyzzy:fool" | chpasswd
chpasswd: (user foo) pam_chauthtok() failed, error:
Authentication token manipulation error
chpasswd: (line 1, user foo) password not changed
chpasswd: (user xyzzy) pam_chauthtok() failed, error:
Authentication token manipulation error
chpasswd: (line 2, user xyzzy) password not changed
答案2
这会将两个系统上存在的 UID > 1000 的所有用户帐户的条目/etc/shadow
从本地系统同步到远程系统(此处称为):remotehost
getent passwd |
awk -F: '$3>1000 {print $1}' |
sort |
join -t : -j 1 -o 2.{1..9} - <(getent shadow | sort) |
ssh remotehost '
cp -fp /etc/shadow /etc/shadow.old &&
join -t : -j 1 -o 1.{1..9} - <(getent shadow | sort) |
awk -F: "!h[\$1]++" - /etc/shadow >/etc/shadow.new &&
: cp -f /etc/shadow.new /etc/shadow
'
我强烈建议您将命令分成几部分,以查看它在管道的每个阶段执行的操作,并且不要从最后一行删除无操作冒号,: cp
直到您确信它按预期工作。
本质上
/etc/passwd
从UID > 1000中提取用户名列表- 使用此列表从中提取相应的行
/etc/shadow
- 复制到远程系统
- 写出
shadow
当前列表中存在的新列表的成员/etc/shadow
- 写出
/etc/shadow
用户名尚未输出的旧行 - 保存原件和新副本
shadow
(在已知地点,以便需要时进行紧急救援) - 将生成的合并文件安装为
/etc/shadow