AWX に Active Directory で認証させて組織やチームと自動で紐づける

はじめに

本エントリでは、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-userDomain UsersAWX Privileged Users
AWX Network Team
fuga-userAWX Privileged UsersAWX Network Team
piyo-userAWX 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 で行います。

DefaultLDAP1 から 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 準拠のオブジェクト(objectClassposixGroup のグループ)や属性(ユーザの 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 GroupLDAP 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 の場合は、上から mailsngivenName です。

したがって、次のように指定します。フォーマットは 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 側に ManagementDevelopment の二つの組織を用意し、それぞれ次のように Adminadmins)と Memberusers)を割り当てています。フォーマットは 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_adminsremove_users は、ログイン時点でのグループの所属状態に準じたロールの 剥奪 の実施有無を指定します。例えば、admins に指定したグループのメンバであるユーザが AWX に一度でもログインすると、AWX 側ではそのユーザは組織に対する Admin ロールを持ちますが、同じユーザがグループのメンバではなくなったあとに再度ログインしたとき、 remove_admins の設定に応じて以下の挙動を取ります。

  • remove_adminstrue だと、そのユーザの Admin ロールは剥奪される
  • remove_adminsfalse だと、そのユーザの Admin ロールは剥奪されない

もう一つの remove_users も、Member ロールに対する同様の処理要否の指定です。

要件次第ではありますが、基本的にはロールの割り当ては Active Directory 側の最新状態に追従する(true にする)ほうが管理上は望ましいでしょう。ただし、グループ外のユーザに手動で割り当てたロールがログイン時に上書きされる可能性もあります。

なお、usersadmins には、グループの DN やそのリストのほかに、truefalsenone も指定できます。それぞれ、LDAP でログインした全ユーザにロールを割り当てる設定、割り当てない設定、割り当ても剥奪も何もしない設定です。要件に合わせて remove_* と組み合せるとよさそうです。

なお、指定した組織が AWX 側に存在しない場合、自動で作成されます。

LDAP Team Map

前述の LDAP Organization Map と似ていますが、こちらは チーム(Team)に対する所属の指定です。

最上位のキーをチーム名(ここでは Server TeamNetwork Team)として、そのチームが所属する組織(organization)、チームのメンバとするグループ(users)、LDAP Organization Mapremove_* と同様のログイン時の所属に応じた自動的な剥奪有無(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 TeamNetwork 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 ですが、二つの組織 ManagementDevelopmentAdmin ロールを持っている管理者として振舞えます。今回は(実際ほぼ意味はないですが)ManagementMember ロールも与えていたので、そのように表示されています。

自身が管理する組織に紐づくオブジェクト(インベントリやプロジェクトなど)は追加や変更、削除が可能ですが、自身が属さない組織(例えばデフォルトの 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 を用いた組織やチームへの紐づけは必須ではなく、例えばとにかく全員ログインさせてしまって後からロールを与えても結果的には同じ状態にはなるわけですが、あらかじめ紐づけを指定しておけば勝手にそのようになるので、当然ながら安全で確実だし、そしてラクです。

前述の通り、プライマリグループの情報が使えない点には注意が必要ですが、比較的柔軟な紐づけができそうなので、触るヒトがおおい環境では便利に使えそうですね。

@kurokobo

くろいです。ギターアンサンブルやら音響やらがフィールドの IT やさんなアルトギター弾き。たまこう 48 期ぎたさん、SFC '07 おんぞう、新日本ギターアンサンブル、Rubinetto。今は野良気味。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です