Linuxのユーザー管理をetcd上でする

    週末の自由研究で、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.confetcd という名前を追加する。を追加します。

    # /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 についても調べまくりました。 そのときに得られた知見については、また別の記事で紹介したいと思います。


    Profile picture

    Shin'ya Ueoka

    B2B向けSaaSを提供する会社の、元Webエンジニア。今はエンジニアリング組織のマネジメントをしている。