週末の自由研究で、Linux のユーザー管理を etcd 上でするサービスを書いてみました。
複数のマシン間でユーザー情報を管理するサービスといえば、LDAP が有名所です。 しかし LDAP は重装備すぎるので、もっと lightweight なサービスができないかと考えてみました。 そこで PoC ですが、etcd をバックエンドにユーザー管理をしてみました。
NSS サービスを書く
Linux のユーザー情報を、/etc/passwd
以外を参照するには、Name Service Switch (NSS)を設定します。
NSS とは、ユーザーやグループ、ホスト名を参照する時の参照データベースを切り替える仕組みです。
その設定は /etc/nsswitch.conf
にあります。
たとえば以下のような設定があります。
# /etc/nsswitch.conf
passwd: files
group: files
...
これは、passwd(ユーザー情報)と group を files サービスを使って参照するということである。
passwd の files サービスは/etc/passwd
を参照し、gropup の files サービスは/etc/group
を参照サービスです。
LDAP を設定するときは、以下のようにldap
サービスを使うよう設定します。
# /etc/nsswitch.conf
passwd: files ldap
group: files ldap
...
今回作成したサービスはetcd
という名前にしたので、/etc/nsswitch.conf
にetcd
という名前を追加する。を追加します。
# /etc/nsswitch.conf
passwd: files etcd
...
NSS を使って実際のエントリを取得する手順を、getpwuid_r()
関数を例に説明します。
getpwuid_r()
関数は、UID を元にユーザー情報を取得する関数です。
ユーザーがgetpwent_r()
を呼び出すと、/etc/nsswitch.conf
に列挙されたそれぞれのサービスのからエントリを取得します。
getpwent_r()
に対応する実装は、/usr/lib/libnss_<service>.so.2
ファイルに記述の _nss_<service>_getpwuid_r()
関数に記述されてます。
以上の命名規則で、これで /etc/nsswitch.conf
に記述されたサービス名から、それぞれの実装に到達することができました。
ユーザー名を参照するときに利用される関数は以下のとおりです。
void setpwent(void);
void endpwent(void);
int getpwent_r(struct passwd *p, char *buf, size_t len, int *errnop);
int getpwnam_r(const char *name, struct passwd *p, char *buf, size_t len, int *errnop);
int getpwuid_r(uid_t uid, struct passwd *p, char *buf, size_t len, int *errnop);
setpwent()
, endpwent()
, はgetpwent_r()
を呼び出すための passwd データベースの接続、終了処理です。
getpwent_r()
は 1 行ずつエントリーを読み込みます。
getpwnam_r()
、getpwuid_r()
はそれぞれ、ユーザ名、ユーザ ID でユーザーエントリーを参照します。
getent passwd
で前者の 3 つの関数が、getent passwd <name>
で getpwent_r()
が、getent passwd <uid>
で getpwuid_r()
を呼び出します。
etcd
という NSS サービスのそれぞれの関数の中身を実装するには、_nss_<service>_<function name>
とう名前で関数を実装します。
今回実装した etcd
サービスに対応する、5 つの関数の定義は以下のとおりとなります。
extern enum nss_status _nss_etcd_setpwent(void);
extern enum nss_status _nss_etcd_endpwent(void);
extern enum nss_status _nss_etcd_getpwent_r(struct passwd *p, char *buf, size_t len, int *errnop);
extern enum nss_status _nss_etcd_getpwnam_r(const char *name, struct passwd *, char *buf, size_t len, int *errnop);
extern enum nss_status _nss_etcd_getpwuid_r(uid_t uid, struct passwd *, char *buf, size_t len, int *errnop);
NSS サービスの実装 : getpwnam_r
の実装例
getpwnam_r()
の実装例を見てゆきます。
まず、_nss_etcd_getpwnam_r()
という名前の関数を宣言して extern
します。
これで外部から、_nss_etcd_getpwnam_r
という名前で関数を参照できます。
// user.h
extern enum nss_status _nss_etcd_getpwnam_r(const char *name, struct passwd *, char *buf, size_t len, int *errnop);
そして実装を.c
ファイルに記述します。
// user.c
enum nss_status _nss_etcd_getpwuid_r(uid_t uid, struct passwd *p, char *buf, size_t len, int *errnop) {
return go_getpwuid(uid, p, buf, len, errnop);
}
生の C は辛いので、殆どの実装を golang で行い、C の関数とのやり取りに cgo を使ってビルドします。
_nss_etcd_getpwnam_r()
が呼び出してるgo_getpwuid
の実装は Go にあります。
// user.go
//export go_getpwuid
func go_getpwuid(uid UID, passwd *C.struct_passwd, buf *C.char, buflen C.size_t, errnop *C.int) nssStatus {
p, err := impl.Getpwuid(uid)
if err == ErrNotFound {
return nssStatusNotfound
} else if err != nil {
return nssStatusUnavail
}
return setCPasswd(p, passwd, buf, buflen, errnop)
}
Getpwuid()
関数は、uid
に一致するユーザーを etcd から取得する関数です。
setCPasswd()
は、Go の struct から C のstruct passwd
および、char *
に展開するヘルパ関数です。
ここでは詳しくは説明しないので、興味のある人は GitHub 上の実装を追ってください。
インストールする
とりあえずの PoC で、参照するのは passwd(グループは見ない)のみですが、ひとまず動かしてみます。 まずはプロジェクトを取得して、必要な依存パッケージをインストールします。
$ go get github.com/ueokande/etcd-passwd
$ cd $GOPATH/src/github.com/ueokande/etcd-passwd
$ go get ./...
そしてプロジェクトをビルドして、システムにインストールします。すると/usr/lib/libnss_etcd.so.2
に etcd サービスが作成されました。
$ make build
$ sudo make install
そして /etc/nsswitch.conf
を編集します。
# /etc/nsswitch.conf
passwd: compat etcd
最後に etcd をローカルで起動します。
$ etcd
ユーザーの追加と参照
ユーザーを追加します。cmd/etcdadduser
にユーザー追加のクライアントコマンドを用意してます。
以下のように使います。
$ go run cmd/etcdadduser/main.go -name peter -uid 10000 -gid 10000 -gecos 'Peter Rabbit'
すると etcd 上にエントリが JSON 形式で追加されます。
$ ETCDCTL_API=3 etcdctl get --print-value-only /etcd-sshd/users/10000
{"Name":"peter","Passwd":"!","UID":10000,"GID":10000,"Gecos":"Peter Rabbit","Dir":"/home/peter","Shell":"/bin/sh"}
ここまでの手順で、新たなユーザーが参照できるようになりました。 実際に追加したユーザーのエントリを参照してみます。
# passwdのキャッシュを無効化
$ sudo nscd --invalidate=passwd
# getentでユーザー名を指定して取得
$ getent passwd peter
peter:!:10000:10000:Peter Rabbit:/home/peter:/bin/sh
# 追加したユーザーになる
$ sudo -u peter id
uid=10000(peter) gid=10000 groups=10000
おわりに
以上で、Go による NSS の実装方法でした。
現在ではまだユーザーデータベースしか参照しません。
グループのデータベースを参照するには、setgrent()
, getgrent()
, endgrent()
, getgrnam_r()
, getgrgid_r()
を実装します。
この NSS サービスを実装するとき、NSS だけではなく cgo についても調べまくりました。 そのときに得られた知見については、また別の記事で紹介したいと思います。