From c3941e8d1a7b272065c5a28202b38d1db029a224 Mon Sep 17 00:00:00 2001 From: Wilton Rodrigues Date: Mon, 14 Oct 2024 16:40:32 -0300 Subject: [PATCH] Add support to multiple LDAP Servers - Add support to multiple LDAP Servers using ServerList option - Updating examples to use ServerList - Refactor print DEBUG params - Updating docs --- examples/conf-from-labels.yml | 8 +- examples/dynamic-conf/ldapAuth-conf.toml | 19 +- examples/dynamic-conf/ldapAuth-conf.yml | 17 +- examples/dynamic-conf/ldapAuth-tls-conf.toml | 10 +- examples/dynamic-conf/ldapAuth-tls-conf.yml | 128 +++++------ ldapauth.go | 216 +++++++++++++------ readme.md | 83 +++---- 7 files changed, 308 insertions(+), 173 deletions(-) diff --git a/examples/conf-from-labels.yml b/examples/conf-from-labels.yml index cadad37..0ee472c 100644 --- a/examples/conf-from-labels.yml +++ b/examples/conf-from-labels.yml @@ -34,10 +34,14 @@ services: - traefik.http.routers.whoami.middlewares=ldap_auth # ldapAuth Options================================================================================= - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.logLevel=DEBUG - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].weight=20 + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[1].url=ldap://ldap2.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[1].port=636 + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[1].weight=10 # AllowedGroups and AllowedUsers are not supported with labels, because multiple value labels are separated with commas # SearchFilter must not escape curly braces when using labels # - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.searchFilter=({{.Attribute}}={{.Username}}) diff --git a/examples/dynamic-conf/ldapAuth-conf.toml b/examples/dynamic-conf/ldapAuth-conf.toml index b771672..48d3631 100644 --- a/examples/dynamic-conf/ldapAuth-conf.toml +++ b/examples/dynamic-conf/ldapAuth-conf.toml @@ -1,13 +1,22 @@ [http.middlewares] [http.middlewares.my-ldapAuth.plugin.ldapAuth] -Attribute = "uid" -BaseDN = "dc=example,dc=com" Enabled = "true" LogLevel = "DEBUG" -Port = "389" -Url = "ldap://ldap.forumsys.com" -AllowedGroups = ["ou=mathematicians,dc=example,dc=com","ou=italians,ou=scientists,dc=example,dc=com"] +Attribute = "uid" +BaseDN = "dc=example,dc=com" +AllowedGroups = [ + "ou=mathematicians,dc=example,dc=com", + "ou=italians,ou=scientists,dc=example,dc=com", +] AllowedUsers = ["euler", "uid=euclid,dc=example,dc=com"] # SearchFilter must escape curly braces when using toml file # https://toml.io/en/v1.0.0#string # SearchFilter = '''(\{\{.Attribute\}\}=\{\{.Username\}\})''' +[[http.middlewares.my-ldapAuth.plugin.ldapAuth.ServerList]] +Url = "ldaps://ldap2.forumsys.com" +Port = "636" +Weight = 10 +[[http.middlewares.my-ldapAuth.plugin.ldapAuth.ServerList]] +Url = "ldap://ldap.forumsys.com" +Port = "389" +Weight = 5 diff --git a/examples/dynamic-conf/ldapAuth-conf.yml b/examples/dynamic-conf/ldapAuth-conf.yml index 11ab1ca..a03d008 100644 --- a/examples/dynamic-conf/ldapAuth-conf.yml +++ b/examples/dynamic-conf/ldapAuth-conf.yml @@ -5,10 +5,8 @@ http: ldapAuth: Enabled: true LogLevel: "DEBUG" - Url: "ldap://ldap.forumsys.com" - Port: 389 - BaseDN: "dc=example,dc=com" Attribute: "uid" + BaseDN: "dc=example,dc=com" AllowedGroups: - ou=mathematicians,dc=example,dc=com - ou=italians,ou=scientists,dc=example,dc=com @@ -18,3 +16,16 @@ http: # SearchFilter must escape curly braces when using yml file # https://yaml.org/spec/1.1/#id872840 # SearchFilter: (\{\{.Attribute\}\}=\{\{.Username\}\}) + ServerList: + - Url: "ldap://ldap.forumsys.com" + Port: 389 + Weight: 10 + - Url: "ldap://ldap4.forumsys.com" + Port: 636 + Weight: 9 + - Url: "ldap://ldap3.forumsys.com" + Port: 389 + Weight: 11 + - Url: "ldap://ldap2.forumsys.com" + Port: 636 + Weight: 12 diff --git a/examples/dynamic-conf/ldapAuth-tls-conf.toml b/examples/dynamic-conf/ldapAuth-tls-conf.toml index 7c2977b..60562e2 100644 --- a/examples/dynamic-conf/ldapAuth-tls-conf.toml +++ b/examples/dynamic-conf/ldapAuth-tls-conf.toml @@ -1,11 +1,15 @@ [http.middlewares] [http.middlewares.my-ldapAuth.plugin.ldapAuth] -Attribute = "uid" -BaseDN = "cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org" Enabled = true LogLevel = "DEBUG" -Port = "636" +Attribute = "uid" +BaseDN = "cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org" +[[http.middlewares.my-ldapAuth.plugin.ldapAuth.ServerList]] Url = "ldaps://ipa.demo1.freeipa.org" +Port = "636" +Weight = 10 +MinVersionTLS = "tls.VersionTLS10" +MaxVersionTLS = "tls.VersionTLS13" CertificateAuthority = ''' -----BEGIN CERTIFICATE----- MIIFWzCCA8OgAwIBAgIBCDANBgkqhkiG9w0BAQsFADA8MRowGAYDVQQKDBFERU1P diff --git a/examples/dynamic-conf/ldapAuth-tls-conf.yml b/examples/dynamic-conf/ldapAuth-tls-conf.yml index c89f559..08f1da8 100644 --- a/examples/dynamic-conf/ldapAuth-tls-conf.yml +++ b/examples/dynamic-conf/ldapAuth-tls-conf.yml @@ -5,66 +5,70 @@ http: ldapAuth: Enabled: true LogLevel: "DEBUG" - Url: "ldaps://ipa.demo1.freeipa.org" - Port: 636 - BaseDN: "cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org" Attribute: "uid" - CertificateAuthority: |- - -----BEGIN CERTIFICATE----- - MIIFWzCCA8OgAwIBAgIBCDANBgkqhkiG9w0BAQsFADA8MRowGAYDVQQKDBFERU1P - MS5GUkVFSVBBLk9SRzEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4X - DTIzMDQyMDEzMzYxNFoXDTI1MDQyMDEzMzYxNFowPDEaMBgGA1UECgwRREVNTzEu - RlJFRUlQQS5PUkcxHjAcBgNVBAMMFWlwYS5kZW1vMS5mcmVlaXBhLm9yZzCCASIw - DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEzBE9i2gqOMM2HKyNnM7Ih5+mv - duVmE5D+5raJtqA1eNZkNrQmSaKwS9cnHGX+2/zSY1FDkZnIhGXySPf0/7fxCuG/ - J9MvRlecGnJTWOCvPIVhkvd5PyTKkClmsk4ojx2IwCU6q2nvy0zvSxhhzd2UpOL6 - y7fNtS3VBYYZjWNEv0K7F+pGtW40MauGDotsP1zQmyVW5J1IszDDlRgTLC6azdBs - +RP0vYCyKkgh1tpWLYfFnQhNVOlja79QcnlKdvnZu4sFdDSvOqext28mBJuCm8ib - HLnQQcxTqg2jMx8AW2zh9F8sMoEsn/mjDHI41oGGsHeZt3j5a8Ab7jtlz8MCAwEA - AaOCAeYwggHiMB8GA1UdIwQYMBaAFKFAgcvZmgX3tnFhcPQ5i4jZ+xE9MEMGCCsG - AQUFBwEBBDcwNTAzBggrBgEFBQcwAYYnaHR0cDovL2lwYS1jYS5kZW1vMS5mcmVl - aXBhLm9yZy9jYS9vY3NwMA4GA1UdDwEB/wQEAwIE8DAdBgNVHSUEFjAUBggrBgEF - BQcDAQYIKwYBBQUHAwIwfAYDVR0fBHUwczBxoDmgN4Y1aHR0cDovL2lwYS1jYS5k - ZW1vMS5mcmVlaXBhLm9yZy9pcGEvY3JsL01hc3RlckNSTC5iaW6iNKQyMDAxDjAM - BgNVBAoMBWlwYWNhMR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHQYD - VR0OBBYEFGsje2irExO4AvvLW6jv2oEkrZfSMIGtBgNVHREEgaUwgaKCFWlwYS5k - ZW1vMS5mcmVlaXBhLm9yZ6A8BgorBgEEAYI3FAIDoC4MLGxkYXAvaXBhLmRlbW8x - LmZyZWVpcGEub3JnQERFTU8xLkZSRUVJUEEuT1JHoEsGBisGAQUCAqBBMD+gExsR - REVNTzEuRlJFRUlQQS5PUkehKDAmoAMCAQGhHzAdGwRsZGFwGxVpcGEuZGVtbzEu - ZnJlZWlwYS5vcmcwDQYJKoZIhvcNAQELBQADggGBADO5SovCVFoVJQOKxrePdh5y - VIQ45UQSjmfXT+FlzbzlX47ejpvdqDKDl0yj5JBUKtKxv3Mj6natUQAVnveRcXlo - mjzEOQsozCaWcCrtnIW8AOny78DjxnSdwPqd/TRV4r2/T2cRndd0GCg6LrQxEdTf - VNKJAMAYin6xmopsarpXwVJVd7YweFUMd7Tu5Tvpde1oubnBtb7ZEGixb6AB200g - lHQroWz6s+a/d7BxsyM0DA5bOk728LqroIJ8m/9xIbnACoyeVdmM5BF/1/cUsX4N - RkRJIfcITNB3zr/4WUldKsM/7bfEA5S0GQUjTd4njt5r7d8j2r6V88maN9ANgXZ4 - Vf1RbjmTOw4OovwGXtRu8DkQ4kSqnyd1COelH48EfGxtOYbzqNNgnip+95mmMoFr - 3BkxKP8G/lQ3kGOYqBIQ+1ICtvx29Smllo87RkJ3KltHy7RKVMZry7inLTqCNBAA - uIdew6R5uJhBBrfjmXGyGjba9wtxDPiPoTa9gGAu7w== - -----END CERTIFICATE----- - -----BEGIN CERTIFICATE----- - MIIEnTCCAwWgAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MRowGAYDVQQKDBFERU1P - MS5GUkVFSVBBLk9SRzEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4X - DTIzMDQyMDEzMzM1NFoXDTQzMDQyMDEzMzM1NFowPDEaMBgGA1UECgwRREVNTzEu - RlJFRUlQQS5PUkcxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTCCAaIw - DQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALLzV665748bW3Da/ZVTZ4BVHrCW - RuuT+7bgT6CZOUMri8F+KuQ6sT+o3hQuyrp4qWn0sU3bO9TCXjkQ4B8uo8ZR3RvR - +2FXENtUQukI4PTXXoKjqJkGrgWVyISfkvNZvsl/bOEtVJ6nh3DBLhYM0HEENccL - 0b1SALdntQwGFJfWkRD0FbjBo7CPxePm7L2VViDMY0cYeUdgETcqc9Zw90gUEqTt - keHqPmBkiOUVk09f3qtdoukRqAvx3nKhUu7vHEf+DJJoQtr3ilUXZQZ/6lKkYl9k - mdwjt+9YeCaKV0s7RI4G+25xo1ZSB3IfMMGISGf/0mOyg4LgWyuuDF/ip5+gI46b - Ol85DrhJAfeYoFbjx+zsoY9mn0kiMBnxg+NkvJitsb5EFexXtqfLLeGjFTu2a9rw - bB6mM3GKmMszwif/i9uO/NeK1LlmN6g1vy07HtjQWh2LUa9AbeIp6s1UUcruCGem - FSzLRmcOY4wi0gGm8Vwg9MRtS6sUe7bfM7uPXwIDAQABo4GpMIGmMB8GA1UdIwQY - MBaAFKFAgcvZmgX3tnFhcPQ5i4jZ+xE9MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P - AQH/BAQDAgHGMB0GA1UdDgQWBBShQIHL2ZoF97ZxYXD0OYuI2fsRPTBDBggrBgEF - BQcBAQQ3MDUwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9pcGEtY2EuZGVtbzEuZnJlZWlw - YS5vcmcvY2Evb2NzcDANBgkqhkiG9w0BAQsFAAOCAYEAH0du998ux4CkH/W9/2l0 - GnnHE5GbBBcGd4zEIxxoe0kYm7MKJjXL9gDRZ3RMseEhy0mAX8cixA7xmg/IFgM9 - TFHoHbTUNgEzLZtOYl5Qccp48ZV1XLrzfK1DorEH6tgza0X2rNJ7RU25sq9i687Y - S0Tt6W3CNkOnQed7blDbxfZJOq7gvqiTFy09a5OXv2AxpkmRrLwFWd/+4Whbsji1 - wiwTD+t7gDTGizqINEsJ3lT+2dDp+mAxPKTd4XiTE4aBPVc4LBxHDnMzqFxa1qzG - v/BL+aa3FkahD/zMm6/B70iApFOFeCrng/1Q7DxUsBWWuzS+oVdm8MEUWtHxANC5 - VG91hbzs4jBAig6AY1hGe49oOabkM1IGhp/TIySAaogA4BFS9DNV1TyNZ4Y9PO61 - JZHjzfXOLIdSlluwsBJem4Lj6Xdw8epzANA0CVnEQ5R1Aql0uRlSsAuhcsleCYJC - 4gbTjx3PDQLm4BUvsNZ62knVDJPvjAX4nOybumpLAVKg - -----END CERTIFICATE----- + BaseDN: "cn=users,cn=accounts,dc=demo1,dc=freeipa,dc=org" + ServerList: + - Url: "ldaps://ipa.demo1.freeipa.org" + Port: 636 + Weight: 10 + MinVersionTLS: "tls.VersionTLS10" + MaxVersionTLS: "tls.VersionTLS13" + CertificateAuthority: |- + -----BEGIN CERTIFICATE----- + MIIFWzCCA8OgAwIBAgIBCDANBgkqhkiG9w0BAQsFADA8MRowGAYDVQQKDBFERU1P + MS5GUkVFSVBBLk9SRzEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4X + DTIzMDQyMDEzMzYxNFoXDTI1MDQyMDEzMzYxNFowPDEaMBgGA1UECgwRREVNTzEu + RlJFRUlQQS5PUkcxHjAcBgNVBAMMFWlwYS5kZW1vMS5mcmVlaXBhLm9yZzCCASIw + DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMEzBE9i2gqOMM2HKyNnM7Ih5+mv + duVmE5D+5raJtqA1eNZkNrQmSaKwS9cnHGX+2/zSY1FDkZnIhGXySPf0/7fxCuG/ + J9MvRlecGnJTWOCvPIVhkvd5PyTKkClmsk4ojx2IwCU6q2nvy0zvSxhhzd2UpOL6 + y7fNtS3VBYYZjWNEv0K7F+pGtW40MauGDotsP1zQmyVW5J1IszDDlRgTLC6azdBs + +RP0vYCyKkgh1tpWLYfFnQhNVOlja79QcnlKdvnZu4sFdDSvOqext28mBJuCm8ib + HLnQQcxTqg2jMx8AW2zh9F8sMoEsn/mjDHI41oGGsHeZt3j5a8Ab7jtlz8MCAwEA + AaOCAeYwggHiMB8GA1UdIwQYMBaAFKFAgcvZmgX3tnFhcPQ5i4jZ+xE9MEMGCCsG + AQUFBwEBBDcwNTAzBggrBgEFBQcwAYYnaHR0cDovL2lwYS1jYS5kZW1vMS5mcmVl + aXBhLm9yZy9jYS9vY3NwMA4GA1UdDwEB/wQEAwIE8DAdBgNVHSUEFjAUBggrBgEF + BQcDAQYIKwYBBQUHAwIwfAYDVR0fBHUwczBxoDmgN4Y1aHR0cDovL2lwYS1jYS5k + ZW1vMS5mcmVlaXBhLm9yZy9pcGEvY3JsL01hc3RlckNSTC5iaW6iNKQyMDAxDjAM + BgNVBAoMBWlwYWNhMR4wHAYDVQQDDBVDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHQYD + VR0OBBYEFGsje2irExO4AvvLW6jv2oEkrZfSMIGtBgNVHREEgaUwgaKCFWlwYS5k + ZW1vMS5mcmVlaXBhLm9yZ6A8BgorBgEEAYI3FAIDoC4MLGxkYXAvaXBhLmRlbW8x + LmZyZWVpcGEub3JnQERFTU8xLkZSRUVJUEEuT1JHoEsGBisGAQUCAqBBMD+gExsR + REVNTzEuRlJFRUlQQS5PUkehKDAmoAMCAQGhHzAdGwRsZGFwGxVpcGEuZGVtbzEu + ZnJlZWlwYS5vcmcwDQYJKoZIhvcNAQELBQADggGBADO5SovCVFoVJQOKxrePdh5y + VIQ45UQSjmfXT+FlzbzlX47ejpvdqDKDl0yj5JBUKtKxv3Mj6natUQAVnveRcXlo + mjzEOQsozCaWcCrtnIW8AOny78DjxnSdwPqd/TRV4r2/T2cRndd0GCg6LrQxEdTf + VNKJAMAYin6xmopsarpXwVJVd7YweFUMd7Tu5Tvpde1oubnBtb7ZEGixb6AB200g + lHQroWz6s+a/d7BxsyM0DA5bOk728LqroIJ8m/9xIbnACoyeVdmM5BF/1/cUsX4N + RkRJIfcITNB3zr/4WUldKsM/7bfEA5S0GQUjTd4njt5r7d8j2r6V88maN9ANgXZ4 + Vf1RbjmTOw4OovwGXtRu8DkQ4kSqnyd1COelH48EfGxtOYbzqNNgnip+95mmMoFr + 3BkxKP8G/lQ3kGOYqBIQ+1ICtvx29Smllo87RkJ3KltHy7RKVMZry7inLTqCNBAA + uIdew6R5uJhBBrfjmXGyGjba9wtxDPiPoTa9gGAu7w== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIEnTCCAwWgAwIBAgIBATANBgkqhkiG9w0BAQsFADA8MRowGAYDVQQKDBFERU1P + MS5GUkVFSVBBLk9SRzEeMBwGA1UEAwwVQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4X + DTIzMDQyMDEzMzM1NFoXDTQzMDQyMDEzMzM1NFowPDEaMBgGA1UECgwRREVNTzEu + RlJFRUlQQS5PUkcxHjAcBgNVBAMMFUNlcnRpZmljYXRlIEF1dGhvcml0eTCCAaIw + DQYJKoZIhvcNAQEBBQADggGPADCCAYoCggGBALLzV665748bW3Da/ZVTZ4BVHrCW + RuuT+7bgT6CZOUMri8F+KuQ6sT+o3hQuyrp4qWn0sU3bO9TCXjkQ4B8uo8ZR3RvR + +2FXENtUQukI4PTXXoKjqJkGrgWVyISfkvNZvsl/bOEtVJ6nh3DBLhYM0HEENccL + 0b1SALdntQwGFJfWkRD0FbjBo7CPxePm7L2VViDMY0cYeUdgETcqc9Zw90gUEqTt + keHqPmBkiOUVk09f3qtdoukRqAvx3nKhUu7vHEf+DJJoQtr3ilUXZQZ/6lKkYl9k + mdwjt+9YeCaKV0s7RI4G+25xo1ZSB3IfMMGISGf/0mOyg4LgWyuuDF/ip5+gI46b + Ol85DrhJAfeYoFbjx+zsoY9mn0kiMBnxg+NkvJitsb5EFexXtqfLLeGjFTu2a9rw + bB6mM3GKmMszwif/i9uO/NeK1LlmN6g1vy07HtjQWh2LUa9AbeIp6s1UUcruCGem + FSzLRmcOY4wi0gGm8Vwg9MRtS6sUe7bfM7uPXwIDAQABo4GpMIGmMB8GA1UdIwQY + MBaAFKFAgcvZmgX3tnFhcPQ5i4jZ+xE9MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0P + AQH/BAQDAgHGMB0GA1UdDgQWBBShQIHL2ZoF97ZxYXD0OYuI2fsRPTBDBggrBgEF + BQcBAQQ3MDUwMwYIKwYBBQUHMAGGJ2h0dHA6Ly9pcGEtY2EuZGVtbzEuZnJlZWlw + YS5vcmcvY2Evb2NzcDANBgkqhkiG9w0BAQsFAAOCAYEAH0du998ux4CkH/W9/2l0 + GnnHE5GbBBcGd4zEIxxoe0kYm7MKJjXL9gDRZ3RMseEhy0mAX8cixA7xmg/IFgM9 + TFHoHbTUNgEzLZtOYl5Qccp48ZV1XLrzfK1DorEH6tgza0X2rNJ7RU25sq9i687Y + S0Tt6W3CNkOnQed7blDbxfZJOq7gvqiTFy09a5OXv2AxpkmRrLwFWd/+4Whbsji1 + wiwTD+t7gDTGizqINEsJ3lT+2dDp+mAxPKTd4XiTE4aBPVc4LBxHDnMzqFxa1qzG + v/BL+aa3FkahD/zMm6/B70iApFOFeCrng/1Q7DxUsBWWuzS+oVdm8MEUWtHxANC5 + VG91hbzs4jBAig6AY1hGe49oOabkM1IGhp/TIySAaogA4BFS9DNV1TyNZ4Y9PO61 + JZHjzfXOLIdSlluwsBJem4Lj6Xdw8epzANA0CVnEQ5R1Aql0uRlSsAuhcsleCYJC + 4gbTjx3PDQLm4BUvsNZ62knVDJPvjAX4nOybumpLAVKg + -----END CERTIFICATE----- diff --git a/ldapauth.go b/ldapauth.go index 95e7e62..d7fe096 100644 --- a/ldapauth.go +++ b/ldapauth.go @@ -16,6 +16,7 @@ import ( "net/url" "os" "reflect" + "sort" "strconv" "strings" "text/template" @@ -31,41 +32,56 @@ var ( LoggerDEBUG = log.New(ioutil.Discard, "DEBUG: ldapAuth: ", log.Ldate|log.Ltime|log.Lshortfile) // LoggerINFO level. LoggerINFO = log.New(ioutil.Discard, "INFO: ldapAuth: ", log.Ldate|log.Ltime|log.Lshortfile) + // LoggerWARNING level. + LoggerWARNING = log.New(ioutil.Discard, "WARNING: ldapAuth: ", log.Ldate|log.Ltime|log.Lshortfile) // LoggerERROR level. LoggerERROR = log.New(ioutil.Discard, "ERROR: ldapAuth: ", log.Ldate|log.Ltime|log.Lshortfile) ) +type LdapServerConfig struct { + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Port uint16 `json:"port,omitempty" yaml:"port,omitempty"` + Weight uint16 `json:"weight,omitempty" yaml:"weight,omitempty"` + StartTLS bool `json:"startTls,omitempty" yaml:"startTls,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` + MinVersionTLS string `json:"minVersionTls,omitempty" yaml:"minVersionTls,omitempty"` + MaxVersionTLS string `json:"maxVersionTls,omitempty" yaml:"maxVersionTls,omitempty"` + CertificateAuthority string `json:"certificateAuthority,omitempty" yaml:"certificateAuthority,omitempty"` +} + // Config the plugin configuration. type Config struct { - Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` - LogLevel string `json:"logLevel,omitempty" yaml:"logLevel,omitempty"` - URL string `json:"url,omitempty" yaml:"url,omitempty"` - Port uint16 `json:"port,omitempty" yaml:"port,omitempty"` - CacheTimeout uint32 `json:"cacheTimeout,omitempty" yaml:"cacheTimeout,omitempty"` - CacheCookieName string `json:"cacheCookieName,omitempty" yaml:"cacheCookieName,omitempty"` - CacheCookiePath string `json:"cacheCookiePath,omitempty" yaml:"cacheCookiePath,omitempty"` - CacheCookieSecure bool `json:"cacheCookieSecure,omitempty" yaml:"cacheCookieSecure,omitempty"` - CacheKey string `json:"cacheKey,omitempty" yaml:"cacheKey,omitempty"` - StartTLS bool `json:"startTls,omitempty" yaml:"startTls,omitempty"` - InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` - MinVersionTLS string `json:"minVersionTls,omitempty" yaml:"minVersionTls,omitempty"` - MaxVersionTLS string `json:"maxVersionTls,omitempty" yaml:"maxVersionTls,omitempty"` - CertificateAuthority string `json:"certificateAuthority,omitempty" yaml:"certificateAuthority,omitempty"` - Attribute string `json:"attribute,omitempty" yaml:"attribute,omitempty"` - SearchFilter string `json:"searchFilter,omitempty" yaml:"searchFilter,omitempty"` - BaseDN string `json:"baseDn,omitempty" yaml:"baseDn,omitempty"` - BindDN string `json:"bindDn,omitempty" yaml:"bindDn,omitempty"` - BindPassword string `json:"bindPassword,omitempty" yaml:"bindPassword,omitempty"` - ForwardUsername bool `json:"forwardUsername,omitempty" yaml:"forwardUsername,omitempty"` - ForwardUsernameHeader string `json:"forwardUsernameHeader,omitempty" yaml:"forwardUsernameHeader,omitempty"` - ForwardAuthorization bool `json:"forwardAuthorization,omitempty" yaml:"forwardAuthorization,omitempty"` - ForwardExtraLdapHeaders bool `json:"forwardExtraLdapHeaders,omitempty" yaml:"forwardExtraLdapHeaders,omitempty"` - WWWAuthenticateHeader bool `json:"wwwAuthenticateHeader,omitempty" yaml:"wwwAuthenticateHeader,omitempty"` - WWWAuthenticateHeaderRealm string `json:"wwwAuthenticateHeaderRealm,omitempty" yaml:"wwwAuthenticateHeaderRealm,omitempty"` - EnableNestedGroupFilter bool `json:"enableNestedGroupsFilter,omitempty" yaml:"enableNestedGroupsFilter,omitempty"` - AllowedGroups []string `json:"allowedGroups,omitempty" yaml:"allowedGroups,omitempty"` - AllowedUsers []string `json:"allowedUsers,omitempty" yaml:"allowedUsers,omitempty"` + Enabled bool `json:"enabled,omitempty" yaml:"enabled,omitempty"` + LogLevel string `json:"logLevel,omitempty" yaml:"logLevel,omitempty"` + ServerList []LdapServerConfig `json:"serverList,omitempty" yaml:"serverList,omitempty"` + CacheTimeout uint32 `json:"cacheTimeout,omitempty" yaml:"cacheTimeout,omitempty"` + CacheCookieName string `json:"cacheCookieName,omitempty" yaml:"cacheCookieName,omitempty"` + CacheCookiePath string `json:"cacheCookiePath,omitempty" yaml:"cacheCookiePath,omitempty"` + CacheCookieSecure bool `json:"cacheCookieSecure,omitempty" yaml:"cacheCookieSecure,omitempty"` + CacheKey string `json:"cacheKey,omitempty" yaml:"cacheKey,omitempty"` + Attribute string `json:"attribute,omitempty" yaml:"attribute,omitempty"` + SearchFilter string `json:"searchFilter,omitempty" yaml:"searchFilter,omitempty"` + BaseDN string `json:"baseDn,omitempty" yaml:"baseDn,omitempty"` + BindDN string `json:"bindDn,omitempty" yaml:"bindDn,omitempty"` + BindPassword string `json:"bindPassword,omitempty" yaml:"bindPassword,omitempty"` + ForwardUsername bool `json:"forwardUsername,omitempty" yaml:"forwardUsername,omitempty"` + ForwardUsernameHeader string `json:"forwardUsernameHeader,omitempty" yaml:"forwardUsernameHeader,omitempty"` + ForwardAuthorization bool `json:"forwardAuthorization,omitempty" yaml:"forwardAuthorization,omitempty"` + ForwardExtraLdapHeaders bool `json:"forwardExtraLdapHeaders,omitempty" yaml:"forwardExtraLdapHeaders,omitempty"` + WWWAuthenticateHeader bool `json:"wwwAuthenticateHeader,omitempty" yaml:"wwwAuthenticateHeader,omitempty"` + WWWAuthenticateHeaderRealm string `json:"wwwAuthenticateHeaderRealm,omitempty" yaml:"wwwAuthenticateHeaderRealm,omitempty"` + EnableNestedGroupFilter bool `json:"enableNestedGroupsFilter,omitempty" yaml:"enableNestedGroupsFilter,omitempty"` + AllowedGroups []string `json:"allowedGroups,omitempty" yaml:"allowedGroups,omitempty"` + AllowedUsers []string `json:"allowedUsers,omitempty" yaml:"allowedUsers,omitempty"` Username string + // params below are deprecated use 'ServerList' instead + URL string `json:"url,omitempty" yaml:"url,omitempty"` + Port uint16 `json:"port,omitempty" yaml:"port,omitempty"` + StartTLS bool `json:"startTls,omitempty" yaml:"startTls,omitempty"` + InsecureSkipVerify bool `json:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty"` + MinVersionTLS string `json:"minVersionTls,omitempty" yaml:"minVersionTls,omitempty"` + MaxVersionTLS string `json:"maxVersionTls,omitempty" yaml:"maxVersionTls,omitempty"` + CertificateAuthority string `json:"certificateAuthority,omitempty" yaml:"certificateAuthority,omitempty"` } // CreateConfig creates the default plugin configuration. @@ -73,18 +89,12 @@ func CreateConfig() *Config { return &Config{ Enabled: true, LogLevel: "INFO", - URL: "", // Supports: ldap://, ldaps:// - Port: 389, // Usually 389 or 636 + ServerList: []LdapServerConfig{}, CacheTimeout: 300, // In seconds, default to 5m CacheCookieName: "ldapAuth_session_token", CacheCookiePath: "", CacheCookieSecure: false, CacheKey: "super-secret-key", - StartTLS: false, - InsecureSkipVerify: false, - MinVersionTLS: "tls.VersionTLS12", - MaxVersionTLS: "tls.VersionTLS13", - CertificateAuthority: "", Attribute: "cn", // Usually uid or sAMAccountname SearchFilter: "", BaseDN: "", @@ -100,6 +110,8 @@ func CreateConfig() *Config { AllowedGroups: nil, AllowedUsers: nil, Username: "", + // deprecated use 'ServerList' instead + URL: "", } } @@ -116,7 +128,31 @@ func New(ctx context.Context, next http.Handler, config *Config, name string) (h LoggerINFO.Printf("Starting %s Middleware...", name) - LogConfigParams(config) + // It means the user is passing the URL directly + if config.URL != "" { + LoggerWARNING.Printf("Passing LDAP Server Attributes directly is deprecated, please use 'ServerList' instead") + server := LdapServerConfig{ + URL: config.URL, + Port: config.Port, + Weight: 1, + StartTLS: config.StartTLS, + InsecureSkipVerify: config.InsecureSkipVerify, + MinVersionTLS: config.MinVersionTLS, + MaxVersionTLS: config.MaxVersionTLS, + CertificateAuthority: config.CertificateAuthority, + } + + config.ServerList = append(config.ServerList, server) + } + + // Rank LDAP servers based on weight. Higher weight, higher precedence + sort.Slice(config.ServerList, func(i, j int) bool { + return config.ServerList[i].Weight > config.ServerList[j].Weight + }) + + settingDefaults(config) + + logConfigParams(config) // Create new session with CacheKey and CacheTimeout. store = sessions.NewCookieStore([]byte(config.CacheKey)) @@ -175,14 +211,30 @@ func (la *LdapAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) { LoggerDEBUG.Println("No session found! Trying to authenticate in LDAP") - conn, err := Connect(la.config) - if err != nil { - LoggerERROR.Printf("%s", err) - RequireAuth(rw, req, la.config, err) - return + var conn *ldap.Conn = nil + var serverInUse LdapServerConfig + errStrings := []string{"All servers in ServerList are down"} + + for i, server := range la.config.ServerList { + attempt := fmt.Sprintf("Attempt %d/%d", i+1, len(la.config.ServerList)) + LoggerDEBUG.Printf(attempt) + + if conn, err = Connect(server); err == nil { + serverInUse = server + break + } + + LoggerERROR.Printf("%v", err) + errStrings = append(errStrings, fmt.Sprintf("%s: %v", attempt, err)) + + if i == len(la.config.ServerList)-1 { + err = fmt.Errorf(strings.Join(errStrings, "\n")) + RequireAuth(rw, req, la.config, err) + return + } } - isValidUser, entry, err := LdapCheckUser(conn, la.config, username, password) + isValidUser, entry, err := LdapCheckUser(conn, la.config, serverInUse, username, password) if !isValidUser { defer conn.Close() @@ -242,7 +294,7 @@ func ServeAuthenicated(la *LdapAuth, session *sessions.Session, rw http.Response } // LdapCheckUser check if user and password are correct. -func LdapCheckUser(conn *ldap.Conn, config *Config, username, password string) (bool, *ldap.Entry, error) { +func LdapCheckUser(conn *ldap.Conn, config *Config, server LdapServerConfig, username, password string) (bool, *ldap.Entry, error) { if config.SearchFilter == "" { LoggerDEBUG.Printf("Running in Bind Mode") userDN := fmt.Sprintf("%s=%s,%s", config.Attribute, username, config.BaseDN) @@ -265,7 +317,7 @@ func LdapCheckUser(conn *ldap.Conn, config *Config, username, password string) ( // Create a new conn to validate user password. This prevents changing the bind made // previously, then LdapCheckUserAuthorized will use same operation mode - _nconn, _ := Connect(config) + _nconn, _ := Connect(server) defer _nconn.Close() // Bind User and password. @@ -397,7 +449,7 @@ func LdapCheckUserGroups(conn *ldap.Conn, config *Config, entry *ldap.Entry, use } // RequireAuth set Auth request. -func RequireAuth(w http.ResponseWriter, req *http.Request, config *Config, err ...error) { +func RequireAuth(w http.ResponseWriter, req *http.Request, config *Config, err error) { LoggerDEBUG.Println(err) w.Header().Set("Content-Type", "text/plain") if config.WWWAuthenticateHeader { @@ -410,12 +462,12 @@ func RequireAuth(w http.ResponseWriter, req *http.Request, config *Config, err . w.WriteHeader(http.StatusUnauthorized) - errMsg := strings.Trim(err[0].Error(), "\x00") + errMsg := strings.Trim(err.Error(), "\x00") _, _ = w.Write([]byte(fmt.Sprintf("%d %s\nError: %s\n", http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized), errMsg))) } // Connect return a LDAP Connection. -func Connect(config *Config) (*ldap.Conn, error) { +func Connect(config LdapServerConfig) (*ldap.Conn, error) { var conn *ldap.Conn = nil var certPool *x509.CertPool var err error = nil @@ -542,15 +594,21 @@ func SetLogger(level string) { switch level { case "ERROR": LoggerERROR.SetOutput(os.Stderr) + case "WARNING": + LoggerERROR.SetOutput(os.Stderr) + LoggerWARNING.SetOutput(os.Stderr) case "INFO": LoggerERROR.SetOutput(os.Stderr) + LoggerWARNING.SetOutput(os.Stderr) LoggerINFO.SetOutput(os.Stdout) case "DEBUG": LoggerERROR.SetOutput(os.Stderr) + LoggerWARNING.SetOutput(os.Stderr) LoggerINFO.SetOutput(os.Stdout) LoggerDEBUG.SetOutput(os.Stdout) default: LoggerERROR.SetOutput(os.Stderr) + LoggerWARNING.SetOutput(os.Stderr) LoggerINFO.SetOutput(os.Stdout) } } @@ -566,24 +624,62 @@ func parseTlsVersion(version string) uint16 { case "tls.VersionTLS13", "VersionTLS13": return tls.VersionTLS13 default: - LoggerINFO.Printf("Version: '%s' doesnt match any value. Using 'tls.VersionTLS10' instead", version) - LoggerINFO.Printf("Please check https://pkg.go.dev/crypto/tls#pkg-constants to a list of valid versions") + LoggerWARNING.Printf("Version: '%s' doesnt match any value. Using 'tls.VersionTLS10' instead", version) + LoggerWARNING.Printf("Please check https://pkg.go.dev/crypto/tls#pkg-constants to a list of valid versions") return tls.VersionTLS10 } } -// LogConfigParams print confs when logLevel is DEBUG. -func LogConfigParams(config *Config) { - /* - Make this to prevent error msg - "Error in Go routine: reflect: call of reflect.Value.NumField on ptr Value" - */ - c := *config +// logConfigParams print confs when logLevel is DEBUG. +func logConfigParams(v interface{}) { + val := reflect.ValueOf(v) + printFieldsRecursive(val, "") +} - v := reflect.ValueOf(c) - typeOfS := v.Type() +// logConfigParams recursively print parameters value. +func printFieldsRecursive(val reflect.Value, indent string) { + val = reflect.Indirect(val) + if val.Kind() == reflect.Struct { + for i := 0; i < val.NumField(); i++ { + field := val.Type().Field(i) + fieldValue := val.Field(i) + + if fieldValue.Kind() == reflect.Struct { + LoggerDEBUG.Printf("%s%s:\n", indent, field.Name) + printFieldsRecursive(fieldValue, indent+" ") + } else if fieldValue.Kind() == reflect.Slice { + LoggerDEBUG.Printf("%s%s:\n", indent, field.Name) + for j := 0; j < fieldValue.Len(); j++ { + printFieldsRecursive(fieldValue.Index(j), indent+" ") + } + if fieldValue.Len() == 0 { + LoggerDEBUG.Printf("%s'[]'\n", indent+" ") + } + } else { + LoggerDEBUG.Printf("%s%s: '%v'\n", indent, field.Name, fieldValue) + } + } + } else { + LoggerDEBUG.Printf("%s'%v'\n", indent, val.Interface()) + } +} + +// settingDefaults to serverList parameters no explicit passed by the user +func settingDefaults(config *Config) { + for i, server := range config.ServerList { + // Default MinVersionTLS value + if server.MinVersionTLS == "" { + config.ServerList[i].MinVersionTLS = "tls.VersionTLS12" + } + + // Default MaxVersionTLS value + if server.MaxVersionTLS == "" { + config.ServerList[i].MaxVersionTLS = "tls.VersionTLS13" + } - for i := 0; i < v.NumField(); i++ { - LoggerDEBUG.Printf(fmt.Sprint(typeOfS.Field(i).Name, " => '", v.Field(i).Interface(), "'")) + // Default Port value + if server.Port == 0 { + config.ServerList[i].Port = 389 + } } } diff --git a/readme.md b/readme.md index bea7c6e..bbfec16 100644 --- a/readme.md +++ b/readme.md @@ -41,10 +41,10 @@ whoami: # ldapAuth Options================================================================= - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.enabled=true - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.logLevel=DEBUG - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 # ================================================================================= ``` @@ -53,10 +53,10 @@ whoami: ```yml [...] labels: - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 ``` ### Search Mode Anonymous Example @@ -64,11 +64,11 @@ labels: ```yml [...] labels: - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.searchFilter=({{.Attribute}}={{.Username}}) + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 ``` ### Search Mode Authenticated Example @@ -76,13 +76,13 @@ labels: ```yml [...] labels: - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.bindDN=uid=tesla,dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.bindPassword=password - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.searchFilter=({{.Attribute}}={{.Username}}) + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 ``` ### Advanced Search Mode Example @@ -90,13 +90,13 @@ labels: ```yml [...] labels: - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.url=ldap://ldap.forumsys.com - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.port=389 - - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.attribute=uid + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.baseDN=dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.bindDN=uid=tesla,dc=example,dc=com - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.bindPassword=password - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.searchFilter=(&(objectClass=person)({{.Attribute}}={{.Username}})) + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].url=ldap://ldap.forumsys.com + - traefik.http.middlewares.ldap_auth.plugin.ldapAuth.serverList[0].port=389 ``` ## Operations Mode @@ -125,18 +125,24 @@ _Optional, Default: `INFO`_ Set `LogLevel` for detailed information about plugin operation. -##### `url` +##### `serverList.url` _Required, Default: `""`_ LDAP server address where queries will be performed. -##### `port` +##### `serverList.port` -_Optional, Default: `389`_ +_Required, Default: `389`_ LDAP server port where queries will be performed. +##### `serverList.weight` + +_Optional, Default: `0`_ + +LDAP server weight to sort `serverList`. Higher weight, higher precedence. + ##### `cacheTimeout` _Optional, Default: `300`_ @@ -164,52 +170,53 @@ _Optional, Default: `super-secret-key`_ The key used to encrypt session cookie information. You `must` use a strong value here. -##### `startTLS` +##### `serverList.startTLS` _Optional, Default: `false`_ If set to true, instruct `ldapAuth` to issue a `StartTLS` request when initializing the connection with the LDAP server. -##### `insecureSkipVerify` +##### `serverList.insecureSkipVerify` _Optional, Default: `false`_ When connecting to a `ldaps` server or `startTLS` is enabled, the connection to the LDAP server is verified to be secure. This option allows `ldapAuth` to proceed and operate even for server connections otherwise considered insecure. -##### `minVersionTLS` +##### `serverList.minVersionTLS` _Optional, Default: `tls.VersionTLS12`_ Contains the minimum TLS version that is acceptable. By default, `TLS 1.2` is currently used as the minimum. `TLS 1.0` is the minimum supported by this package. This option value must match [crypto/tls](https://pkg.go.dev/crypto/tls#pkg-constants) versions constants. -##### `maxVersionTLS` +##### `serverList.maxVersionTLS` _Optional, Default: `tls.VersionTLS13`_ Contains the maximum TLS version that is acceptable. By default, the maximum version supported by this package is used, which is currently `TLS 1.3`. This option value must match [crypto/tls](https://pkg.go.dev/crypto/tls#pkg-constants) versions constants. -##### `certificateAuthority` +##### `serverList.certificateAuthority` _Optional, Default: `""`_ -The `certificateAuthority` option should contain one or more PEM-encoded certificates to use to establish a connection with the LDAP server if the connection uses TLS but the certificate was signed by a custom Certificate Authority. +The `serverList.certificateAuthority` option should contain one or more PEM-encoded certificates to use to establish a connection with the LDAP server if the connection uses TLS but the certificate was signed by a custom Certificate Authority. Example: ```yml - certificateAuthority: |- - -----BEGIN CERTIFICATE----- - MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV - BAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl - Y29kaW5nIG1lPyAgVGhpcyBpcyBvbmx5IGEgdGVzdCEhITERMA8GA1UEBwwISGFt - aWx0b24xETAPBgNVBAgMCFBlbWJyb2tlMQswCQYDVQQGEwJCTTEPMA0GCSqGSIb3 - DQEJARYAMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJ9WRanG/fUvcfKiGl - EL4aRLjGt537mZ28UU9/3eiJeJznNSOuNLnF+hmabAu7H0LT4K7EdqfF+XUZW/2j - RKRYcvOUDGF9A7OjW7UfKk1In3+6QDCi7X34RE161jqoaJjrm/T18TOKcgkkhRzE - apQnIDm0Ea/HVzX/PiSOGuertwIDAQABMAsGCSqGSIb3DQEBBQOBgQBzMJdAV4QP - Awel8LzGx5uMOshezF/KfP67wJ93UW+N7zXY6AwPgoLj4Kjw+WtU684JL8Dtr9FX - ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ - 98TwDIK/39WEB/V607As+KoYazQG8drorw== - -----END CERTIFICATE----- + ServerList: + CertificateAuthority: |- + -----BEGIN CERTIFICATE----- + MIIB9TCCAWACAQAwgbgxGTAXBgNVBAoMEFF1b1ZhZGlzIExpbWl0ZWQxHDAaBgNV + BAsME0RvY3VtZW50IERlcGFydG1lbnQxOTA3BgNVBAMMMFdoeSBhcmUgeW91IGRl + Y29kaW5nIG1lPyAgVGhpcyBpcyBvbmx5IGEgdGVzdCEhITERMA8GA1UEBwwISGFt + aWx0b24xETAPBgNVBAgMCFBlbWJyb2tlMQswCQYDVQQGEwJCTTEPMA0GCSqGSIb3 + DQEJARYAMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCJ9WRanG/fUvcfKiGl + EL4aRLjGt537mZ28UU9/3eiJeJznNSOuNLnF+hmabAu7H0LT4K7EdqfF+XUZW/2j + RKRYcvOUDGF9A7OjW7UfKk1In3+6QDCi7X34RE161jqoaJjrm/T18TOKcgkkhRzE + apQnIDm0Ea/HVzX/PiSOGuertwIDAQABMAsGCSqGSIb3DQEBBQOBgQBzMJdAV4QP + Awel8LzGx5uMOshezF/KfP67wJ93UW+N7zXY6AwPgoLj4Kjw+WtU684JL8Dtr9FX + ozakE+8p06BpxegR4BR3FMHf6p+0jQxUEAkAyb/mVgm66TyghDGC6/YkiKoZptXQ + 98TwDIK/39WEB/V607As+KoYazQG8drorw== + -----END CERTIFICATE----- ``` ##### `attribute`