|
3. 클라이언트 설정
클라이언트는 ssh와 NFS 클라이언트 소프트웨어를 가지고 있어야 합니다. 또한 커널이 NFS 파일 시스템을 지원하도록 모듈 또는 커널 자체에 기능을 내장하고 있어야 하겠죠. 대부분의 배포본 커널도 별 문제없이 사용할 수 있을 것입니다.
암호화 NFS 마운트를 위한 기본적인 단계:
mountd와 nfsd가 어떤 포트에서 실행되고 있는지 찾는다.
ssh로 로컬 포워딩을 설정한다.
localhost에서 NFS 볼륨을 마운트한다.
이제 NFS 서버의 이름은 "nfs1"이라고 가정하고, rpcinfo -p nfs1을 클라이언트 쪽에서 실행 하면 다음과 비슷한 결과를 보여줄 것입니다:
program vers proto port
100000 2 tcp 111 portmapper
100000 2 udp 111 portmapper
100024 1 udp 946 status
100024 1 tcp 946 status
100003 2 udp 2049 nfs
100003 2 tcp 2049 nfs
100005 1 udp 945 mountd
100005 2 udp 945 mountd
100005 1 tcp 945 mountd
100005 2 tcp 945 mountd
결과를 보면 "nfs1"이 2049 포트로 NFS 접속을 기다리고 있다는 것을 알수 있으며, mountd는 945번 포트인 것을 알 수 있습니다. NFS 서버는 대부분 2049번 포트를 사용하지만 mountd는 특별히 지정하지 않는 경우에는 달라질 수 있습니다.
이제 NFS 서버 포트를 알았으니 ssh 터널을 만들어 봅시다. ssh 포트 포워딩은 처음 사용하는 사람들에게는 조금 복잡해 보일 수 있습니다. 기본적인 방법은 다음과 같습니다:
ssh -f -L 로컬포트:서버:서버포트 -l 사용자명 서버 명령어
-L는 로컬 포워딩을 의미합니다. 로컬포트는 로컬 프로세스가 사용할 포트 번호이고, 서버는 접속할 서버, 서버포트는 rpcinfo에서 찾은 서버의 포트 번호 입니다. -l 사용자명은 서버에 접속할 수 있는 로그인명, 명령어는 서버상에서 실행할 명령어, -f는 명령이 백그라운드로 실행됨을 의미 합니다. 다음 예를 보세요:
ssh -f -L 2818:nfs1:2049 -l james nfs1 sleep 300
위 명령은 james라는 로그인명을 사용하고 nfs1의 2049번 포트를 로컬의 2818로 포워딩 한 것입니다. ssh는 일단 300초(5분)동안 만들어진 터널을 유지하는데 그 사이에 이 터널에 어떠한 접속이 있다면 그 연결이 끊어지기 전까지는 계속 유지하게 됩니다. mountd를 위한 또 하나의 터널을 만듭시다:
ssh -f -L 3045:nfs1:945 -l james nfs1 sleep 300
OpenSSH를 사용한다면 몇가지 특별한 옵션을 사용할 수 있는데 실제로 명령은 다음과 같습니다:
ssh -f -c blowfish -L 2818:nfs1:2049 -L 3045:nfs1:945 \
-l james nfs1 /bin/sleep 86400
이 설정은 두개의 터널을 동시에 만들고, 접속 대기 시간을 86400(하루)로 한 것입니다. 암호화 방식은 Blowfish를 사용하도록 했는데 이것이 기본값 보다 빠르게 작동합니다.
이제 만들어진 터널을 사용해서 NFS 볼륨을 마운트 합니다:
mount -t nfs -o tcp,port=2818,mountport=3045 \
localhost:/opt/export/users /mnt/nfs/sshmount
localhost로 포워딩된 포트를 사용해서 마운트 했습니다. 실제로는 2818 포트는 "nfs1"의 2049 포트와의 터널이고 3045번 포트는 역시 "nfs1"의 945번 포트와의 터널입니다. 디렉토리는 서버상에서 /etc/exports에서 export해준 디렉토리를 사용해야 겠죠? 정말 암호화되는지는 sniffer나 Ethereal같은 툴로 확인해 볼 수 있습니다. 모든것이 암호화 됩니다! :)
NFS 마운트를 좀더 쉽게 하기 위해서 클라이언트의 /etc/fstab에 다음과 같이 추가합니다:
localhost:/opt/export/users /mnt/nfs/sshmount nfs \
tcp,rsize=8192,wsize=8192,intr,rw,bg,nosuid, \
port=2818,mountport=3045,noauto
이제 터널이 만들어져 있다면 mount /mnt/nfs/sshmount만 입력하면 되겠죠.
이미 언급했듯이 클라이언트는 리눅스에서만 가능합니다. 그 이유는 umount명령에서 *BSD와 솔라리스는 mountport 옵션을 지원하지 않기 때문입니다. 만약 지원하게 된다면 어떠한 UNIX 클라이언트에서도 사용 가능하게 될것입니다.
클라이언트 쪽의 방화벽 정책은 서버와 비슷하지만 ssh와 sunrpc 포트의 접속이 이미 이루어진 후에 설정이 가능합니다. iptables 사용예:
iptables -A INPUT -i eth0 -p tcp ! -syn -s nfs1 -sport ssh \
-j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp -d nfs1 -dport ssh \
-j ACCEPT
iptables -A INPUT -i eth0 -p tcp ! -syn -s nfs1 -sport 111 \
-j ACCEPT
iptables -A OUTPUT -o eth0 -p tcp -d nfs1 -dport 111 \
-j ACCEPT
ipchains 사용예:
ipchains -A input -i eth0 -p tcp ! -y -s nfs1 -sport ssh \
-j ACCEPT
ipchains -A output -i eth0 -p tcp -d nfs1 -dport ssh \
-j ACCEPT
ipchains -A input -i eth0 -p tcp ! -y -s nfs1 -sport 111 \
-j ACCEPT
ipchains -A output -i eth0 -p tcp -d nfs1 -dport 111 \
-j ACCEPT
3.1. 좀더 쉽게
클라이언트에서 이 과정을 간편하게 하기 위해 펄 스크립트(nfs_ssh_fw.pl)를 만들었습니다. 이것을 부팅시 실행되도록 rc.local에 넣어 둘 수 있습니다. 설정은 스크립트의 시작부분의 변수를 수정하면 됩니다.
이 스크립트는 rpcinfo를 호출해서 어떤 포트가 서버에서 사용되고 있는지 확인하고 그에 알맞게 ssh 옵션은 지정합니다. ssh 로그인 암호를 직접 이 스크립트에 넣을 수는 있으나 보안상 좋지 않으므로 인증키를 만들어 사용하는 것이 좋습니다.
ssh-keygen -t dsa
사용자 이름을 "jdstrand"라고 가정하고 키를 /home/jdstrand/.ssh/id_dsa_nfs에 저장합니다. 암호 문구를 물어보는 프롬프트에서는 그냥 "Enter"를 입력합니다.
이제 id_dsa_nfs와 id_dsa_nfs.pub의 한쌍의 키가 만들어 졌습니다. 공개키인 id_dsa_nfs.pub를 서버의 계정으로 복사한 후에 authorized_keys2 파일에 추가합니다.
cd /home/james/.ssh
cat ./id_dsa_nfs.pub >> ./authorized_keys2
이제 클라이언트는 서버로 암호 입력없이 로그인 할 수 있습니다. 다음을 실행해 봅니다:
ssh -2 -i /home/jdstrand/.ssh/id_dsa_nfs -l james nfs1
위 명령은 ssh 프로토콜 버전 2를 사용하고 어떤 키를 사용할 것인지 직접 지정한 것입니다. 이제 이것을 nfs_ssh_fw.pl의 $ssh_opts 변수에 넣어 주면 되겠죠.
참고: http://kldp.org/~eunjea/ssh/
모든 것이 잘 작동된다고 확인되었다면 이제 이것을 /etc/init.d/rc.local(또는 부팅시 실행되는 스크립트)에 넣습니다:
echo "Mounting the encrypted filesystem"
/usr/local/sbin/nfs_ssh_fw.pl &
sleep 10
mount /mnt/nfs/sshmount
sleep 명령은 ssh에게 터널을 만드는데 시간을 줍니다. 만약 /etc/fstab 파일에 bg옵션을 사용하였다면 sleep 명령이 필요 없으나 오류 메세지가 부팅시에 표시될 수 있습니다.
4. 맺음말
모든 NFS 클라이언트를 ssh 암호화 하는 것은 그 댓가가 있습니다. 즉, 그 만큼 CPU를 사용하게 되겠죠. 그러나 암호화에 어떤 해독 열쇠(cipher)를 사용하는 가에 따라 다릅니다. 예를 들어 제 경우 Blowfish 방식을 사용할때가 3des를 사용 했을 때보다 300% 속도가 더 빨랐습니다.
이미 언급한대로 클라이언트는 리눅스에서만 가능합니다.
Autofs는 작동하지 않습니다.
파일 공유 서비스는 대부분의 네트워크에서 필요 불가결한 것입니다. NFS는 쉽고 오래된 프로토콜이지만 불행하게도 보안상 안전하지 못합니다. ssh를 이용한 NFS 연결은 spoof 공격을 방지하고 네트워크 상에서 오고 가는 모든 데이터를 암호화 하며, 강력한 인증을 사용합니다. 또 다른 장점은 새로운 프로토콜(예를 들어 AFS나 IPSec)을 배울 필요가 없다는 것입니다. IPSec와는 달리 ssh를 사용한 NFS는 NAT와도 작동합니다.
이 문서에서 사용한 스크립트는 리눅스, FreeBSD 4.3 서버 그리고 다양한 리눅스 클라이언트에서 테스트 했습니다. TCP 암호화 NFS의 연결 속도는 당연히 일반적인 NFS 연결보다는 느리지만 실제적으로 적용하는데 큰 무리가 없습니다.
5. nfs_ssh_fw.pl
#!/usr/bin/perl -w
use strict;
use File::Basename;
## CONFIGURATION
my $nfs_server = "nfs1"; # the nfs server to connect to
my $nfs_server_user = "james"; # a valid username on the nfs server
my $use_version = "2"; # nfs-user-server uses 2, otherwise 3
# would be better. Check output of
# 'rpcinfo -p '
my $nfsd_client_port = "2818"; # we will port forward nfsd here
my $mountd_client_port = "3045"; # we will port forward mountd here
my $sleep_length = "86400"; # how long to sleep before restarting
# 86400 secs is one day. Note
# this is overridden if a command is
# specified in the server's
# authorized_keys2 file
# need to keep '-f', can also specify encryption algorithm, the ssh version
# and the id key
my $ssh_opts = "-f -c blowfish -2 -i /home/james/.ssh/id_dsa_nfs";
my %rpcinfo_col = ( # change as per output of rpcinfo -p
'program' => '0',
'version' => '1',
'protocol' => '2',
'port' => '3',
'daemon' => '4'
);
## END CONFIGURATION
# not much should need to change below here
my $prog_name = basename($0);
my $nfsd_server_port = "";
my $mountd_server_port = "";
# for signals
$SIG{INT} = sub { die "$0 interrupted and dying (does not kill ssh)\n" };
my $first_time = 1;
while (1) {
if ($first_time) {
print "$prog_name: Starting ssh/nfs forwarding-\n";
$first_time = 0;
} else {
print "$prog_name: Restarting ssh/nfs forwarding-\n";
}
# first, get the rpcinfo
my @rpcinfo = `rpcinfo -p $nfs_server`;
print "My rpcinfo =\n @rpcinfo";
# now get the nfsd and mountd port numbers
foreach (@rpcinfo) {
my @line = split;
if ($line[$rpcinfo_col{"version"}] eq $use_version &&
$line[$rpcinfo_col{"daemon"}] eq "nfs" &&
$line[$rpcinfo_col{"protocol"}] eq "tcp") {
$nfsd_server_port = $line[$rpcinfo_col{"port"}];
print (" nfsd port = $nfsd_server_port");
} elsif ($line[$rpcinfo_col{"version"}] eq $use_version &&
$line[$rpcinfo_col{"daemon"}] eq "mountd" &&
$line[$rpcinfo_col{"protocol"}] eq "tcp") {
$mountd_server_port = $line[$rpcinfo_col{"port"}];
print (", mountd port = $mountd_server_port\n");
}
}
# now run ssh (if this fails, we get the error message and
# retry). This should run all the time. This also won't die
# unless the nfs mount is done.
`/usr/bin/ssh $ssh_opts -L \
$nfsd_client_port:$nfs_server:$nfsd_server_port -L \
$mountd_client_port:$nfs_server:$mountd_server_port -l \
$nfs_server_user $nfs_server /bin/sleep $sleep_length`;
}
|