はじめに
本エントリでは、AWX のユーザ認証のバックエンドに Active Directory を LDAP サーバとして利用する場合の構成を取り上げます。
単にログインできるようにするだけではあまり工夫のしどころがないので、もう少し踏み込んだユースケースを想定して、Active Directory 側の グループ と AWX の 組織 や チーム とのマッピングも構成します。
AWX 側の ロール と組み合わせることで、Active Directory のグループに応じた、いわゆる RBAC を実現できます。
環境と要件
今回は、ドメイン ansible.local
下に次のような OU(🏢)とグループ(🏠)、ユーザ(🧑)を用意しました。理由は後述しますが、すべてのユーザのプライマリグループはデフォルトの Domain Users
で、図示したグループは二つ目以降の追加グループ扱いにしてあります。
今回は、ざっくり次のような要件の充足を目指します。
- AWX にログインできるメンバを特定の OU に限定する
- グループによって AWX 側での役割を変える
- グループによって AWX 側でのチームを変える
- グループによって AWX 側で実行できるジョブテンプレートを変える
つまり、いくつか具体例を示すと、次のような状態です。
- グループ
Lab Admins
のメンバは AWX のシステム管理者としてふるまえる - グループ
Auditors
のメンバは AWX のシステム監査人としてふるまえる - グループ
Server Team
のメンバは、サーバチームに明示的に許可されたジョブテンプレートしか実行できない。Network Team
のメンバも同様にネットワークチームに許可されたものしか触れない - OU
Hands-On Lab
に所属していないユーザはそもそも AWX にログインできない
Active Directory 側のグループ構成の注意点
今回の要件のように、Active Directory 側のグループ情報 を利用して AWX 側で何らかの判定や紐づけを行いたい場合、ユーザのプライマリグループの情報は利用できない 点に注意が必要です(Active Directory の仕様に基づく挙動のため、別の LDAP サーバの実装を利用する場合はこの限りではありません)。
例えば、Active Directory で以下のようなユーザを構成したとします。
ユーザ名 | プライマリグループ | 追加のグループ |
hoge-user | Domain Users | AWX Privileged Users AWX Network Team |
fuga-user | AWX Privileged Users | AWX Network Team |
piyo-user | AWX Network Team | (なし) |
これらのユーザを AWX で認証しても、AWX 側では プライマリグループの所属情報は無視 されます。
つまり、AWX の LDAP の構成で、例えば上記のうち AWX Privileged Users
グループの所属ユーザを AWX 側で管理者に 指定した場合でも、実際に管理者になるのは hoge-user
だけ で、fuga-user
は対象外です。同様に、AWX Network Team
に AWX 側で何らかの権限 を与えたとしても、piyo-user
は含まれない 状態になります。
技術的には、これは AWX が使っている Django の django-auth-ldap
の仕様と(LDAP サーバとしての)Active Directory の仕様の組み合わせの結果です。詳細は割愛しますが、ざっくりとは、グループの所属を調べる際に django-auth-ldap
が グループオブジェクトの member
属性 を利用するのに対し、Active Directory のグループオブジェクトの member
属性には、そのグループをプライマリグループにしているユーザの情報が含まれない ことに起因しています。Active Directory 側では、プライマリグループの所属情報はユーザオブジェクトの primaryGroupID
属性でしか保持されていませんが、django-auth-ldap
はそれを参照しないようです。
標準的な構成では、Active Directory ドメインに作成したユーザのプライマリグループは Domain Users
なので、それ以外のグループで制御するように組めば問題にはなりにくいですが、プライマリグループを変更している構成では注意が必要です。
AWX での LDAP の設定
AWX の LDAP の設定は、Settings
> LDAP settings
で行います。
Default
と LDAP1
から LDAP5
までの複数の設定を構成できますが、認証ソースにしたい LDAP サービスがひとつだけの場合は、Default
で構成するとよいでしょう。複数設定した場合は、番号順に順次認証が試行されていくようです。
このページの設定を説明している公式ドキュメントは以下です。画面が古いままに見えますが、設定項目自体はあまり変わりません。
以下、各項目の簡単な説明と設定例です。順番は画面と一部変えています。
LDAP Server URI / LDAP Start TLS
LDAP サーバの URI です。ドキュメントに書かれていませんが、スペースやカンマ区切りで複数のサーバを指定できます。
ldap://dc1.ansible.local, ldap://dc2.ansible.local
暗号化を行う場合は、LDAPS を使うなら ldaps://
に、Start TLS を使うなら LDAP Start TLS を On にします。今回は暗号化していません。
LDAP Bind DN / LDAP Bind Password
LDAP サーバへの問い合わせに利用されるバインドユーザの Distinguished Name(DN)とそのパスワードです。Active Directory では匿名バインドは許可されていないため、指定は必須です。今回は bind
ユーザを利用しています。
CN=bind,CN=Users,DC=ansible,DC=local
バインドユーザは、LDAP としてのバインドのためだけに利用するので、後述の LDAP Require Group など AWX 的な要件を満たしたユーザである必要はありません。
LDAP Group Type
Active Directory を利用する場合は、ActiveDirectoryGroupType
または NestedActiveDirectoryGroupType
を選択します。ユーザのグループの所属状態を検索する際に、グループがネストされた構成を許容 しない 場合は前者、許容 する 場合は後者を指定します。
この設定の選択肢は、Django の django-auth-ldap
に準拠しています。各タイプの詳細は Django のドキュメント で説明されています。
今回はネストされたグループへの所属情報も参照させる必要がある(後述しますが、AWX Users
グループのメンバとして他のグループを含めている)ので、後者にしています。
NestedActiveDirectoryGroupType
ドキュメントの記載通り、上記二つのタイプは実際にはそれぞれ MemberDNGroupType(member_attr='member')
と NestedMemberDNGroupType(member_attr='member')
と同義のようで、必要な引数とその値を後述の LDAP Group Type Parameters で与えさえすればどちらでも機能します。PosixGroupType
は POSIX 準拠のオブジェクト(objectClass
が posixGroup
のグループ)や属性(ユーザの gidNumber
やグループの memberUid
)を前提に動作するため、それ用に構成していない Active Directory では正しく機能しません。
LDAP Group Type Parameters
前述の LDAP Group Type で指定したタイプに合わせてパラメータを指定します。このパラメータは、Django の django-auth-ldap
のクラスのコンストラクタに渡す引数に対応しています。
前述の通り、Active Directory を利用する場合は LDAP Group Type は次のどちらかが適切です。
この二つには、キーとして name_attr
の指定が必要です。この値には LDAP オブジェクトの持つ属性(Attributes)のうち、グループ名を保持している属性名を指定します。Active Diretory の場合は cn
です。
したがって、次の値を設定します。フォーマットは JSON です。
{
"name_attr": "cn"
}
LDAP Require Group / LDAP Deny Group
AWX にログイン できる グループ、または できない グループを指定します。
どちらも空欄にした場合は、Active Directory の全ユーザが AWX にログインできる状態になります。逆に両方を指定した場合は、相補的に機能し、LDAP Require Group のメンバのうち LDAP Deny Group のメンバでないユーザのみがログインできるようになります。
Active Directory は、通常のシナリオでは AWX 専用とは考えにくく、であれば必ずしも Active Directory の全ユーザが AWX にログインできてよいとは限りません。仕様上は、AWX 側で明示的に何らかの権限を与えない限りは、ログインできるだけでは何の操作権限も持たないので、何も見えないし何も実行できませんが、最小特権の原則に倣えば、ログインすべきユーザだけがログインできる状態が本来です。
したがって、ここでは例として、次のグループのメンバ のみ が AWX にログインできるように構成します。
Lab Admins
Org Admins
Auditors
Server Team
Network Team
ただし、設定欄 LDAP Require Group と LDAP Deny Group は、ひとつ のグループしか設定できません。このため、これらのグループ自体をメンバに持つグループ AWX Users
を作成し、それを LDAP Require Group に指定することで、要件を充足させています。
CN=AWX Users,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local
なお、このような ネストされたグループへの所属情報 を認識させるには、前述の LDAP Group Type で接頭辞 Nested
を持つタイプを選択する必要があります。
LDAP User Search
ユーザを検索するクエリを指定します。今回は次のように設定しています。フォーマットは JSON です。
[
"OU=Hands-On Lab,DC=ansible,DC=local",
"SCOPE_SUBTREE",
"(sAMAccountName=%(user)s)"
]
これは次の意味です。
- コンテナ
OU=Hands-On Lab,DC=ansible,DC=local
の - 配下のすべて(
SCOPE_SUBTREE
)のオブジェクトで - 属性
sAMAccountName
の値が入力されたユーザ名と一致するもの
今回の構成では、冒頭の図の通り AWX にログインしうるユーザが二つの OU に分かれて所属していますが、さらに上位をみれば、すべてのユーザは OU=Hands-On Lab
配下に存在していると言えます。また、OU=Hands-On Lab
配下には、検索範囲に含めるべきでないコンテナは存在していません。よって、検索パスに上位の OU を指定し、そこから再帰検索を意味するスコープ SUBTREE
で検索させれば、目的のユーザを確実に見つけられることになります。
別解として、今回の構成では、次の設定でも結果的には同等に機能します。
[
[
"OU=Management,OU=Hands-On Lab,DC=ansible,DC=local",
"SCOPE_ONELEVEL",
"(sAMAccountName=%(user)s)"
],
[
"OU=Development,OU=Hands-On Lab,DC=ansible,DC=local",
"SCOPE_ONELEVEL",
"(sAMAccountName=%(user)s)"
]
]
これは、次の二つの検索クエリを結合させたものです。
- ひとつめ
- コンテナ
OU=Management,OU=Hands-On Lab,DC=ansible,DC=local
の - 直下(
SCOPE_ONELEVEL
)のオブジェクトで - 属性
sAMAccountName
の値が入力されたユーザ名と一致するもの
- コンテナ
- ふたつめ
- コンテナ
OU=Development,OU=Hands-On Lab,DC=ansible,DC=local
の - 直下(
SCOPE_ONELEVEL
)のオブジェクトで - 属性
sAMAccountName
の値が入力されたユーザ名と一致するもの
- コンテナ
ここでは、上位の OU ではなく、下位の OU それぞれを検索しています。すべてのユーザは二つの OU の直下に存在しているため、OU の直下を検索するクエリを二つの OU で実行して結合すれば、目的の範囲の検索が過不足なく行えることになります。
上位エントリを指定した再帰検索と、下位エントリを指定した検索の結合では、構成次第で検索の範囲や検索のコストも変わってきます。実際の構成に合わせて必要充分な検索ができるクエリにできるとよさそうです。
LDAP User DN Template
ユーザオブジェクトの DN が全ユーザで共通のパタンである場合に、決め打ちで指定するための設定です。プレースホルダ %(user)s
がユーザ名に置換されて利用されます。
LDAP を利用した認証の過程では、認証したいユーザの DN が厳密に特定される必要があります。この目的で、LDAP クライアントは事前に定義した範囲を事前に定義した条件で検索します。この検索の範囲と条件の指定が、前述の LDAP User Search の設定です。
一方で、認証したいユーザが例外なくすべて特定の OU の直下に存在している場合などの特定の状況下では、ユーザの検索を行わなくとも、ユーザ名から一意の DN を導出できます。このような場合に、ユーザの DN のパタンをあらかじめ指定しておけるのがこの設定です。指定すると、LDAP User Search に基づく検索は行われなくなります。
今回は、所属する OU によってユーザの DN が異なるため、事前に決め打ちができません。したがって、この設定は利用せず、空欄のままにします。
LDAP Group Search
グループの検索範囲を指定します。入力されたユーザの所属グループ情報は、ここで指定した範囲の中で検索されます。
今回は次のように指定しています。フォーマットは JSON です。
[
"OU=Hands-On Lab,DC=ansible,DC=local",
"SCOPE_SUBTREE",
"(objectClass=group)"
]
書式は前述の LDAP User Search と一緒で、
- コンテナ
OU=Hands-On Lab,DC=ansible,DC=local
の - 配下のすべて(
SCOPE_SUBTREE
)のオブジェクトで - 属性
objectClass
の値がgroup
のもの
を表現しています。Active Directory のグループは、属性 objectClass
の値は group
です。
なお、LDAP Group Search には、前述の LDAP User Search とは異なり、複数の検索範囲の結合ができません。このため、すべてのグループが含まれるような(広めの)検索範囲の指定が必要です。
LDAP User Attribute Map
AWX 側でのユーザの属性と LDAP のユーザオブジェクトの属性の対応付けを指定する設定です。
AWX 側のユーザは、ユーザ名以外に次の属性を持っています。
- メールアドレス(
email
) - 姓(
last_name
) - 名(
first_name
)
LDAP のユーザオブジェクトも、これらに相当する属性を持っています。Active Directory の場合は、上から mail
、sn
、givenName
です。
したがって、次のように指定します。フォーマットは JSON です。
{
"first_name": "givenName",
"last_name": "sn",
"email": "mail"
}
これにより、Active Directory 側で保持している姓名やメールアドレスの情報が、認証後に AWX 側でも表示されるようになります。
LDAP User Flags By Group
AWX 側で System Administrator または System Auditor として扱う グループ の DN を指定します。
この設定は、AWX で新規にユーザを作成する際に指定できる User Type の設定に相当します。前者は全権を持つスーパユーザで、後者は監査ユーザ(すべての情報を閲覧できるが変更や実行はできない権限のユーザ)です。
指定したグループに所属するユーザは、指定通りのタイプのユーザとしてログインできます。ここでは、Lab Admins
グループに所属するユーザを System Administrator に、Auditors
グループのユーザを System Auditor に、それぞれ指定しています。フォーマットは JSON です。
{
"is_superuser": [
"CN=Lab Admins,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local"
],
"is_system_auditor": [
"CN=Auditors,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local"
]
}
それぞれリスト型が渡せるため、複数のグループの指定も可能です。いずれの指定にも含まれないグループのユーザは、Normal User 扱いになります。
LDAP Organization Map
AWX 側の 組織(Organization)に、Active Directory のグループのユーザを紐づける設定です。グループのユーザに対して、組織に対する Admin または Member のロールを自動で持たせられます。
今回は、Active Directory の OU に準じて AWX 側に Management と Development の二つの組織を用意し、それぞれ次のように Admin(admins
)と Member(users
)を割り当てています。フォーマットは JSON で、最上位のキーが組織名です。
{
"Management": {
"admins": [
"CN=Org Admins,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove_admins": true,
"users": [
"CN=Org Admins,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove_users": true
},
"Development": {
"admins": [
"CN=Org Admins,OU=Management,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove_admins": true,
"users": [
"CN=Server Team,OU=Development,OU=Hands-On Lab,DC=ansible,DC=local",
"CN=Network Team,OU=Development,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove_users": true
}
}
remove_admins
と remove_users
は、ログイン時点でのグループの所属状態に準じたロールの 剥奪 の実施有無を指定します。例えば、admins
に指定したグループのメンバであるユーザが AWX に一度でもログインすると、AWX 側ではそのユーザは組織に対する Admin ロールを持ちますが、同じユーザがグループのメンバではなくなったあとに再度ログインしたとき、 remove_admins
の設定に応じて以下の挙動を取ります。
remove_admins
がtrue
だと、そのユーザの Admin ロールは剥奪されるremove_admins
がfalse
だと、そのユーザの Admin ロールは剥奪されない
もう一つの remove_users
も、Member ロールに対する同様の処理要否の指定です。
要件次第ではありますが、基本的にはロールの割り当ては Active Directory 側の最新状態に追従する(true
にする)ほうが管理上は望ましいでしょう。ただし、グループ外のユーザに手動で割り当てたロールがログイン時に上書きされる可能性もあります。
なお、users
と admins
には、グループの DN やそのリストのほかに、true
や false
、none
も指定できます。それぞれ、LDAP でログインした全ユーザにロールを割り当てる設定、割り当てない設定、割り当ても剥奪も何もしない設定です。要件に合わせて remove_*
と組み合せるとよさそうです。
なお、指定した組織が AWX 側に存在しない場合、自動で作成されます。
LDAP Team Map
前述の LDAP Organization Map と似ていますが、こちらは チーム(Team)に対する所属の指定です。
最上位のキーをチーム名(ここでは Server Team と Network Team)として、そのチームが所属する組織(organization
)、チームのメンバとするグループ(users
)、LDAP Organization Map の remove_*
と同様のログイン時の所属に応じた自動的な剥奪有無(remove
)を設定できます。
{
"Server Team": {
"organization": "Development",
"users": [
"CN=Server Team,OU=Development,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove": true
},
"Network Team": {
"organization": "Development",
"users": [
"CN=Network Team,OU=Development,OU=Hands-On Lab,DC=ansible,DC=local"
],
"remove": true
}
}
組織と同様、指定したチームが AWX 側に存在しない場合、自動で作成されます。
動作の確認
設定できたら、動きを確認します。
あらかじめ空のチーム Server Team と Network Team を作成し、各チーム用のジョブテンプレートも作って、チームに Execute ロールを割り当てておきます。ジョブテンプレートへのロールの割り当ては、ジョブテンプレートの Access
タブか、チームの Roles
タブから行えます。
System Administrator タイプの確認
Lab Admins
グループに所属するユーザでログインします。LDAP 設定画面での指定通り、System Administrator タイプのユーザとして振舞えます。
実際、System Administrator でないとアクセスできない設定画面も操作可能です。
System Auditor タイプの確認
Auditors
グループに所属するユーザでログインすると、System Auditor タイプのユーザとして振舞えます。
System Auditor なので、すべての画面にアクセスできますが、閲覧できるだけで、実行や変更はできません。
組織の Admin ロールの確認
Org Admins
グループに所属するユーザでログインすると、ユーザタイプは Normal User ですが、二つの組織 Management と Development の Admin ロールを持っている管理者として振舞えます。今回は(実際ほぼ意味はないですが)Management の Member ロールも与えていたので、そのように表示されています。
自身が管理する組織に紐づくオブジェクト(インベントリやプロジェクトなど)は追加や変更、削除が可能ですが、自身が属さない組織(例えばデフォルトの Default)のオブジェクトは不可視で、参照すらできません。
組織の Member ロールの確認
Server Team
グループか Network Team
グループに所属するユーザでログインすると、Normal User タイプですが、LDAP 設定画面での指定通り、AWX のチームのひとりとしてのロールを持てています。
今回は事前にチームに対してジョブテンプレートの Execute ロールを与えていたので、このユーザでは所属するチーム用のジョブテンプレートの実行は可能です。それ以外のジョブテンプレートやオブジェクトについては一切の権限を持たないので、何も見えず、何もできません。
ログインが許可されていないユーザの確認
OU Hands-On Lab
配下でないユーザは、ユーザの検索範囲に含まれないので、そもそもログインできません。 OU Hands-On Lab
配下のユーザであっても、AWX Users
グループのメンバでない場合はログインは拒否されます。
その他の確認
LDAP でログインしたユーザは、一覧画面などで LDAP
フラグが付与されて区別されます。姓名も確認できます。
ユーザごとのページでは、メールアドレスも連携できていることがわかります。
トラブルシュート
LDAP の認証は、前述の通り Django が司っているため、詳細なログは AWX の Pod のうち awx-web
コンテナから確認できます。
LDAP として行われている検索のクエリと結果も含まれるので、意図した状態にならないときの調査に役立ちます。以下は例えば lab-admin1
でログインしたときのログです。ユーザが検索され、awx users
グループと lab admins
グループのメンバであること、それ以外のグループのメンバではないことが正しく判定されている様子が見て取れます。
$ kubectl -n awx logs -f deployment.apps/awx -c awx-web --tail=100
...
django_auth_ldap search_s('OU=Management,OU=Hands-On Lab,DC=ansible,DC=local', 1, '(sAMAccountName=%(user)s)') returned 1 objects: cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap search_s('OU=Development,OU=Hands-On Lab,DC=ansible,DC=local', 1, '(sAMAccountName=%(user)s)') returned 0 objects:
django_auth_ldap search_s('OU=Hands-On Lab,DC=ansible,DC=local', 2, '(&(objectClass=group)(|(member=cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local)))') returned 1 objects: cn=lab admins,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap search_s('OU=Hands-On Lab,DC=ansible,DC=local', 2, '(&(objectClass=group)(|(member=cn=lab admins,ou=management,ou=hands-on lab,dc=ansible,dc=local)))') returned 1 objects: cn=awx users,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap search_s('OU=Hands-On Lab,DC=ansible,DC=local', 2, '(&(objectClass=group)(|(member=cn=awx users,ou=management,ou=hands-on lab,dc=ansible,dc=local)))') returned 0 objects:
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is a member of cn=awx users,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap Populating Django user lab-admin1
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is a member of cn=lab admins,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is not a member of cn=auditors,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is not a member of cn=org admins,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is not a member of cn=org admins,ou=management,ou=hands-on lab,dc=ansible,dc=local
django_auth_ldap cn=lab-admin1,ou=management,ou=hands-on lab,dc=ansible,dc=local is not a member of cn=org admins,ou=management,ou=hands-on lab,dc=ansible,dc=local
...
まとめ
AWX の認証を Active Directory で行うための設定と、Active Directory 側のグループの所属に応じて AWX 側での組織やチームの所属を制御する方法を紹介しました。
LDAP を用いた組織やチームへの紐づけは必須ではなく、例えばとにかく全員ログインさせてしまって後からロールを与えても結果的には同じ状態にはなるわけですが、あらかじめ紐づけを指定しておけば勝手にそのようになるので、当然ながら安全で確実だし、そしてラクです。
前述の通り、プライマリグループの情報が使えない点には注意が必要ですが、比較的柔軟な紐づけができそうなので、触るヒトがおおい環境では便利に使えそうですね。