前回の記事で、
AWS ALB の LDAP 認証連携の構築
https://qiita.com/batatch/items/1577d453944070c19365
dexidp による OIDC-LDAP変換で ALB 認証連携を実施したのですが、実は LDAPサーバとなる ADにはそのままでは連携できなかったので、dexidpに少し手入れが必要でした。
通常、Apache等で認証連携するときは、管理アカウントでバインドしてから、ユーザを検索/認証するという動きをするのですが、対象の ADでは、直接ユーザアカウントでバインドするというものでした。
そのため、Apacheなどの静的設定で利用されておらず、プログラムで独自に接続する形になっていました。
この対応についての記録になります。
LDAP方式の違い
この LDAPの方式の違いがどういうものか分からず、調べてみたところ、静的な設定ファイルで 2種類の方式に対応している例がありました。
PostgreSQL 文書 / 20.10. LDAP認証
https://www.postgresql.jp/document/11/html/auth-ldap.html
このマニュアルでは、2種類の LDAP方式を以下のように説明しています。
- シンプル・バインド・モード (simple bind mode)
- サーチ+バインド・モード (search+bind mode)
後者が Apache等の設定でよく見る管理者アカウントで事前にバインドするもの、
前者が今回の ADのようにユーザアカウントで直接バインドするものです。
設定ファイルには、ldapbinddn/ldapbindpasswd でバインド用の管理者アカウントを設定すれば「サーチ+バインド・モード」、
ldapprefix/ldapsuffix を設定すれば「シンプル・バインド・モード」となるようです。
シンプル・バインド・モードでは、prefix username suffix という形でユーザアカウントでバインドするようになります。
これを参考に、dexidp の LDAPコネクタに設定項目を追加できるようにしてみます。
dexidp へ LDAP シンプル・バインド・モードを追加
PostgreSQLを参考に、以下の形で設定できるようにします。
bindDNPrefix、bindDNSuffix というパラメータで、シンプル・バインド・モードの場合に、ユーザアカウントの前後に追加するラベルを指定します。
issuer: https://oidc.example.com/dex
storage:
type: sqlite3
config:
file: dex/dex.db
web:
http: 0.0.0.0:5556
connectors:
- type: ldap
name: OpenLDAP
id: ldap
config:
host: ldap.example.com:389
insecureNoSSL: true
# bindDN: cn=admin,dc=example,dc=org
# bindPW: admin
bindDNPrefix: "cn=" ★追加
bindDNSuffix: ",ou=People,dc=example,dc=org" ★追加
usernamePrompt: User ID
userSearch:
baseDN: ou=People,dc=example,dc=com
username: mail
idAttr: DN
emailAttr: mail
nameAttr: cn
groupSearch:
baseDN: ou=Groups,dc=example,dc=com
userAttr: DN
groupAttr: member
nameAttr: cn
staticClients:
- id: app-id
redirectURIs:
- 'https://app.example.com/oauth2/idpresponse'
name: 'App'
secret: ZXhhbXBsZS1hcHAtc2VjcmV0
ソースの変更内容は以下のとおり。
diff --git a/connector/ldap/ldap.go b/connector/ldap/ldap.go
index aed7319..f1ddf01 100644
--- a/connector/ldap/ldap.go
+++ b/connector/ldap/ldap.go
@@ -31,6 +31,9 @@ import (
// rootCA: /etc/dex/ldap.ca
// bindDN: uid=seviceaccount,cn=users,dc=example,dc=com
// bindPW: password
+// # for ldap simple bind mode.
+// bindDNPrefix: uid=
+// bindDNSuffix: ,cn=users,dc=example,dc=com
// userSearch:
// # Would translate to the query "(&(objectClass=person)(uid=<username>))"
// baseDN: cn=users,dc=example,dc=com
@@ -81,6 +84,11 @@ type Config struct {
BindDN string `json:"bindDN"`
BindPW string `json:"bindPW"`
+ // BindDNPrefix and BindDNSuffix for LDAP Simple bind mode.
+ // The connector uses BindDNPrefix username BindDNSuffix as bind username.
+ BindDNPrefix string `json:"bindDNPrefix"`
+ BindDNSuffix string `json:"bindDNSuffix"`
+
// UsernamePrompt allows users to override the username attribute (displayed
// in the username/password prompt). If unset, the handler will use
// "Username".
@@ -249,7 +257,7 @@ func (c *Config) openConnector(logger log.Logger) (*ldapConnector, error) {
if !ok {
return nil, fmt.Errorf("groupSearch.Scope unknown value %q", c.GroupSearch.Scope)
}
- return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger}, nil
+ return &ldapConnector{*c, userSearchScope, groupSearchScope, tlsConfig, logger, "", []byte{}}, nil
}
type ldapConnector struct {
@@ -261,6 +269,9 @@ type ldapConnector struct {
tlsConfig *tls.Config
logger log.Logger
+
+ uid string
+ pass []byte
}
var (
@@ -296,12 +307,26 @@ func (c *ldapConnector) do(ctx context.Context, f func(c *ldap.Conn) error) erro
}
defer conn.Close()
+ var (
+ bindDN string
+ bindPW string
+ )
+ bindDN = c.BindDN
+ bindPW = c.BindPW
+
+ if c.BindDNPrefix != "" || c.BindDNSuffix != "" {
+ bindDN = fmt.Sprintf("%s%s%s", c.BindDNPrefix, c.uid, c.BindDNSuffix)
+ bindPW = string(c.pass)
+ }
+
+ c.logger.Debugf("ldap: - bindDN=<%s>", bindDN)
+
// If bindDN and bindPW are empty this will default to an anonymous bind.
- if err := conn.Bind(c.BindDN, c.BindPW); err != nil {
- if c.BindDN == "" && c.BindPW == "" {
+ if err := conn.Bind(bindDN, bindPW); err != nil {
+ if bindDN == "" && bindPW == "" {
return fmt.Errorf("ldap: initial anonymous bind failed: %v", err)
}
- return fmt.Errorf("ldap: initial bind for user %q failed: %v", c.BindDN, err)
+ return fmt.Errorf("ldap: initial bind for user %q failed: %v", bindDN, err)
}
return f(conn)
@@ -414,6 +439,10 @@ func (c *ldapConnector) userEntry(conn *ldap.Conn, username string) (user ldap.E
}
func (c *ldapConnector) Login(ctx context.Context, s connector.Scopes, username, password string) (ident connector.Identity, validPass bool, err error) {
+
+ c.uid = username
+ c.pass = []byte(password)
+
// make this check to avoid unauthenticated bind to the LDAP server.
if password == "" {
return connector.Identity{}, false, nil
上記の差分を patch 適用して dockerビルドするには、前回の Dockerfileを以下のように変更します。
FROM golang:1.13-alpine AS build-env
RUN apk add --no-cache --update alpine-sdk
RUN mkdir -p /go/src/github.com/dexidp \
&& cd /go/src/github.com/dexidp \
&& git clone https://github.com/dexidp/dex.git -b v2.21.0 --depth 1
COPY ldap_simple_bind.patch /go/src/github.com/dexidp/dex ★追加
RUN cd /go/src/github.com/dexidp/dex && git apply ldap_simple_bind.patch ★追加
RUN cd /go/src/github.com/dexidp/dex && make release-binary
FROM alpine:3.10
RUN apk add --update ca-certificates openssl sqlite
USER root
COPY --from=build-env /go/bin/dex /usr/local/bin/dex
COPY --from=build-env /go/src/github.com/dexidp/dex/web /web
VOLUME /dex
WORKDIR /
ENTRYPOINT ["dex"]
GitHubにもアップしました。
https://github.com/batatch/dex/tree/feature/ldap_simple_bind