github.com/hashicorp/vault/sdk@v0.13.0/helper/ldaputil/config.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package ldaputil 5 6 import ( 7 "crypto/tls" 8 "crypto/x509" 9 "encoding/pem" 10 "errors" 11 "fmt" 12 "strings" 13 "text/template" 14 15 "github.com/go-ldap/ldap/v3" 16 capldap "github.com/hashicorp/cap/ldap" 17 "github.com/hashicorp/errwrap" 18 "github.com/hashicorp/go-secure-stdlib/tlsutil" 19 "github.com/hashicorp/vault/sdk/framework" 20 ) 21 22 var ldapDerefAliasMap = map[string]int{ 23 "never": ldap.NeverDerefAliases, 24 "finding": ldap.DerefFindingBaseObj, 25 "searching": ldap.DerefInSearching, 26 "always": ldap.DerefAlways, 27 } 28 29 // ConfigFields returns all the config fields that can potentially be used by the LDAP client. 30 // Not all fields will be used by every integration. 31 func ConfigFields() map[string]*framework.FieldSchema { 32 return map[string]*framework.FieldSchema{ 33 "anonymous_group_search": { 34 Type: framework.TypeBool, 35 Default: false, 36 Description: "Use anonymous binds when performing LDAP group searches (if true the initial credentials will still be used for the initial connection test).", 37 DisplayAttrs: &framework.DisplayAttributes{ 38 Name: "Anonymous group search", 39 }, 40 }, 41 "url": { 42 Type: framework.TypeString, 43 Default: "ldap://127.0.0.1", 44 Description: "LDAP URL to connect to (default: ldap://127.0.0.1). Multiple URLs can be specified by concatenating them with commas; they will be tried in-order.", 45 DisplayAttrs: &framework.DisplayAttributes{ 46 Name: "URL", 47 }, 48 }, 49 50 "userdn": { 51 Type: framework.TypeString, 52 Description: "LDAP domain to use for users (eg: ou=People,dc=example,dc=org)", 53 DisplayAttrs: &framework.DisplayAttributes{ 54 Name: "User DN", 55 }, 56 }, 57 58 "binddn": { 59 Type: framework.TypeString, 60 Description: "LDAP DN for searching for the user DN (optional)", 61 DisplayAttrs: &framework.DisplayAttributes{ 62 Name: "Name of Object to bind (binddn)", 63 }, 64 }, 65 66 "bindpass": { 67 Type: framework.TypeString, 68 Description: "LDAP password for searching for the user DN (optional)", 69 DisplayAttrs: &framework.DisplayAttributes{ 70 Sensitive: true, 71 }, 72 }, 73 74 "groupdn": { 75 Type: framework.TypeString, 76 Description: "LDAP search base to use for group membership search (eg: ou=Groups,dc=example,dc=org)", 77 DisplayAttrs: &framework.DisplayAttributes{ 78 Name: "Group DN", 79 }, 80 }, 81 82 "groupfilter": { 83 Type: framework.TypeString, 84 Default: "(|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))", 85 Description: `Go template for querying group membership of user (optional) 86 The template can access the following context variables: UserDN, Username 87 Example: (&(objectClass=group)(member:1.2.840.113556.1.4.1941:={{.UserDN}})) 88 Default: (|(memberUid={{.Username}})(member={{.UserDN}})(uniqueMember={{.UserDN}}))`, 89 DisplayAttrs: &framework.DisplayAttributes{ 90 Name: "Group Filter", 91 }, 92 }, 93 94 "groupattr": { 95 Type: framework.TypeString, 96 Default: "cn", 97 Description: `LDAP attribute to follow on objects returned by <groupfilter> 98 in order to enumerate user group membership. 99 Examples: "cn" or "memberOf", etc. 100 Default: cn`, 101 DisplayAttrs: &framework.DisplayAttributes{ 102 Name: "Group Attribute", 103 Value: "cn", 104 }, 105 }, 106 107 "userfilter": { 108 Type: framework.TypeString, 109 Default: "({{.UserAttr}}={{.Username}})", 110 Description: `Go template for LDAP user search filer (optional) 111 The template can access the following context variables: UserAttr, Username 112 Default: ({{.UserAttr}}={{.Username}})`, 113 DisplayAttrs: &framework.DisplayAttributes{ 114 Name: "User Search Filter", 115 }, 116 }, 117 118 "upndomain": { 119 Type: framework.TypeString, 120 Description: "Enables userPrincipalDomain login with [username]@UPNDomain (optional)", 121 DisplayAttrs: &framework.DisplayAttributes{ 122 Name: "User Principal (UPN) Domain", 123 }, 124 }, 125 126 "username_as_alias": { 127 Type: framework.TypeBool, 128 Default: false, 129 Description: "If true, sets the alias name to the username", 130 }, 131 132 "userattr": { 133 Type: framework.TypeString, 134 Default: "cn", 135 Description: "Attribute used for users (default: cn)", 136 DisplayAttrs: &framework.DisplayAttributes{ 137 Name: "User Attribute", 138 Value: "cn", 139 }, 140 }, 141 142 "certificate": { 143 Type: framework.TypeString, 144 Description: "CA certificate to use when verifying LDAP server certificate, must be x509 PEM encoded (optional)", 145 DisplayAttrs: &framework.DisplayAttributes{ 146 Name: "CA certificate", 147 EditType: "file", 148 }, 149 }, 150 151 "client_tls_cert": { 152 Type: framework.TypeString, 153 Description: "Client certificate to provide to the LDAP server, must be x509 PEM encoded (optional)", 154 DisplayAttrs: &framework.DisplayAttributes{ 155 Name: "Client certificate", 156 EditType: "file", 157 }, 158 }, 159 160 "client_tls_key": { 161 Type: framework.TypeString, 162 Description: "Client certificate key to provide to the LDAP server, must be x509 PEM encoded (optional)", 163 DisplayAttrs: &framework.DisplayAttributes{ 164 Name: "Client key", 165 EditType: "file", 166 }, 167 }, 168 169 "discoverdn": { 170 Type: framework.TypeBool, 171 Description: "Use anonymous bind to discover the bind DN of a user (optional)", 172 DisplayAttrs: &framework.DisplayAttributes{ 173 Name: "Discover DN", 174 }, 175 }, 176 177 "insecure_tls": { 178 Type: framework.TypeBool, 179 Description: "Skip LDAP server SSL Certificate verification - VERY insecure (optional)", 180 DisplayAttrs: &framework.DisplayAttributes{ 181 Name: "Insecure TLS", 182 }, 183 }, 184 185 "starttls": { 186 Type: framework.TypeBool, 187 Description: "Issue a StartTLS command after establishing unencrypted connection (optional)", 188 DisplayAttrs: &framework.DisplayAttributes{ 189 Name: "Issue StartTLS", 190 }, 191 }, 192 193 "tls_min_version": { 194 Type: framework.TypeString, 195 Default: "tls12", 196 Description: "Minimum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'. Defaults to 'tls12'", 197 DisplayAttrs: &framework.DisplayAttributes{ 198 Name: "Minimum TLS Version", 199 }, 200 AllowedValues: []interface{}{"tls10", "tls11", "tls12", "tls13"}, 201 }, 202 203 "tls_max_version": { 204 Type: framework.TypeString, 205 Default: "tls12", 206 Description: "Maximum TLS version to use. Accepted values are 'tls10', 'tls11', 'tls12' or 'tls13'. Defaults to 'tls12'", 207 DisplayAttrs: &framework.DisplayAttributes{ 208 Name: "Maximum TLS Version", 209 }, 210 AllowedValues: []interface{}{"tls10", "tls11", "tls12", "tls13"}, 211 }, 212 213 "deny_null_bind": { 214 Type: framework.TypeBool, 215 Default: true, 216 Description: "Denies an unauthenticated LDAP bind request if the user's password is empty; defaults to true", 217 }, 218 219 "case_sensitive_names": { 220 Type: framework.TypeBool, 221 Description: "If true, case sensitivity will be used when comparing usernames and groups for matching policies.", 222 }, 223 224 "use_token_groups": { 225 Type: framework.TypeBool, 226 Default: false, 227 Description: "If true, use the Active Directory tokenGroups constructed attribute of the user to find the group memberships. This will find all security groups including nested ones.", 228 }, 229 230 "use_pre111_group_cn_behavior": { 231 Type: framework.TypeBool, 232 Description: "In Vault 1.1.1 a fix for handling group CN values of different cases unfortunately introduced a regression that could cause previously defined groups to not be found due to a change in the resulting name. If set true, the pre-1.1.1 behavior for matching group CNs will be used. This is only needed in some upgrade scenarios for backwards compatibility. It is enabled by default if the config is upgraded but disabled by default on new configurations.", 233 }, 234 235 "request_timeout": { 236 Type: framework.TypeDurationSecond, 237 Description: "Timeout, in seconds, for the connection when making requests against the server before returning back an error.", 238 Default: "90s", 239 }, 240 241 "connection_timeout": { 242 Type: framework.TypeDurationSecond, 243 Description: "Timeout, in seconds, when attempting to connect to the LDAP server before trying the next URL in the configuration.", 244 Default: "30s", 245 }, 246 247 "dereference_aliases": { 248 Type: framework.TypeString, 249 Description: "When aliases should be dereferenced on search operations. Accepted values are 'never', 'finding', 'searching', 'always'. Defaults to 'never'.", 250 Default: "never", 251 AllowedValues: []interface{}{"never", "finding", "searching", "always"}, 252 }, 253 254 "max_page_size": { 255 Type: framework.TypeInt, 256 Description: "If set to a value greater than 0, the LDAP backend will use the LDAP server's paged search control to request pages of up to the given size. This can be used to avoid hitting the LDAP server's maximum result size limit. Otherwise, the LDAP backend will not use the paged search control.", 257 Default: 0, 258 }, 259 } 260 } 261 262 /* 263 * Creates and initializes a ConfigEntry object with its default values, 264 * as specified by the passed schema. 265 */ 266 func NewConfigEntry(existing *ConfigEntry, d *framework.FieldData) (*ConfigEntry, error) { 267 var hadExisting bool 268 var cfg *ConfigEntry 269 270 if existing != nil { 271 cfg = existing 272 hadExisting = true 273 } else { 274 cfg = new(ConfigEntry) 275 } 276 277 if _, ok := d.Raw["anonymous_group_search"]; ok || !hadExisting { 278 cfg.AnonymousGroupSearch = d.Get("anonymous_group_search").(bool) 279 } 280 281 if _, ok := d.Raw["username_as_alias"]; ok || !hadExisting { 282 cfg.UsernameAsAlias = d.Get("username_as_alias").(bool) 283 } 284 285 if _, ok := d.Raw["url"]; ok || !hadExisting { 286 cfg.Url = strings.ToLower(d.Get("url").(string)) 287 } 288 289 if _, ok := d.Raw["userfilter"]; ok || !hadExisting { 290 userfilter := d.Get("userfilter").(string) 291 if userfilter != "" { 292 // Validate the template before proceeding 293 _, err := template.New("queryTemplate").Parse(userfilter) 294 if err != nil { 295 return nil, errwrap.Wrapf("invalid userfilter: {{err}}", err) 296 } 297 } 298 299 cfg.UserFilter = userfilter 300 } 301 302 if _, ok := d.Raw["userattr"]; ok || !hadExisting { 303 cfg.UserAttr = strings.ToLower(d.Get("userattr").(string)) 304 } 305 306 if _, ok := d.Raw["userdn"]; ok || !hadExisting { 307 cfg.UserDN = d.Get("userdn").(string) 308 } 309 310 if _, ok := d.Raw["groupdn"]; ok || !hadExisting { 311 cfg.GroupDN = d.Get("groupdn").(string) 312 } 313 314 if _, ok := d.Raw["groupfilter"]; ok || !hadExisting { 315 groupfilter := d.Get("groupfilter").(string) 316 if groupfilter != "" { 317 // Validate the template before proceeding 318 _, err := template.New("queryTemplate").Parse(groupfilter) 319 if err != nil { 320 return nil, errwrap.Wrapf("invalid groupfilter: {{err}}", err) 321 } 322 } 323 324 cfg.GroupFilter = groupfilter 325 } 326 327 if _, ok := d.Raw["groupattr"]; ok || !hadExisting { 328 cfg.GroupAttr = d.Get("groupattr").(string) 329 } 330 331 if _, ok := d.Raw["upndomain"]; ok || !hadExisting { 332 cfg.UPNDomain = d.Get("upndomain").(string) 333 } 334 335 if _, ok := d.Raw["certificate"]; ok || !hadExisting { 336 certificate := d.Get("certificate").(string) 337 if certificate != "" { 338 if err := validateCertificate([]byte(certificate)); err != nil { 339 return nil, errwrap.Wrapf("failed to parse server tls cert: {{err}}", err) 340 } 341 } 342 cfg.Certificate = certificate 343 } 344 345 if _, ok := d.Raw["client_tls_cert"]; ok || !hadExisting { 346 clientTLSCert := d.Get("client_tls_cert").(string) 347 cfg.ClientTLSCert = clientTLSCert 348 } 349 350 if _, ok := d.Raw["client_tls_key"]; ok || !hadExisting { 351 clientTLSKey := d.Get("client_tls_key").(string) 352 cfg.ClientTLSKey = clientTLSKey 353 } 354 355 if cfg.ClientTLSCert != "" && cfg.ClientTLSKey != "" { 356 if _, err := tls.X509KeyPair([]byte(cfg.ClientTLSCert), []byte(cfg.ClientTLSKey)); err != nil { 357 return nil, errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err) 358 } 359 } else if cfg.ClientTLSCert != "" || cfg.ClientTLSKey != "" { 360 return nil, fmt.Errorf("both client_tls_cert and client_tls_key must be set") 361 } 362 363 if _, ok := d.Raw["insecure_tls"]; ok || !hadExisting { 364 cfg.InsecureTLS = d.Get("insecure_tls").(bool) 365 } 366 367 if _, ok := d.Raw["tls_min_version"]; ok || !hadExisting { 368 cfg.TLSMinVersion = d.Get("tls_min_version").(string) 369 _, ok = tlsutil.TLSLookup[cfg.TLSMinVersion] 370 if !ok { 371 return nil, errors.New("invalid 'tls_min_version'") 372 } 373 } 374 375 if _, ok := d.Raw["tls_max_version"]; ok || !hadExisting { 376 cfg.TLSMaxVersion = d.Get("tls_max_version").(string) 377 _, ok = tlsutil.TLSLookup[cfg.TLSMaxVersion] 378 if !ok { 379 return nil, fmt.Errorf("invalid 'tls_max_version'") 380 } 381 } 382 if cfg.TLSMaxVersion < cfg.TLSMinVersion { 383 return nil, fmt.Errorf("'tls_max_version' must be greater than or equal to 'tls_min_version'") 384 } 385 386 if _, ok := d.Raw["starttls"]; ok || !hadExisting { 387 cfg.StartTLS = d.Get("starttls").(bool) 388 } 389 390 if _, ok := d.Raw["binddn"]; ok || !hadExisting { 391 cfg.BindDN = d.Get("binddn").(string) 392 } 393 394 if _, ok := d.Raw["bindpass"]; ok || !hadExisting { 395 cfg.BindPassword = d.Get("bindpass").(string) 396 } 397 398 if _, ok := d.Raw["deny_null_bind"]; ok || !hadExisting { 399 cfg.DenyNullBind = d.Get("deny_null_bind").(bool) 400 } 401 402 if _, ok := d.Raw["discoverdn"]; ok || !hadExisting { 403 cfg.DiscoverDN = d.Get("discoverdn").(bool) 404 } 405 406 if _, ok := d.Raw["case_sensitive_names"]; ok || !hadExisting { 407 cfg.CaseSensitiveNames = new(bool) 408 *cfg.CaseSensitiveNames = d.Get("case_sensitive_names").(bool) 409 } 410 411 usePre111GroupCNBehavior, ok := d.GetOk("use_pre111_group_cn_behavior") 412 if ok { 413 cfg.UsePre111GroupCNBehavior = new(bool) 414 *cfg.UsePre111GroupCNBehavior = usePre111GroupCNBehavior.(bool) 415 } 416 417 if _, ok := d.Raw["use_token_groups"]; ok || !hadExisting { 418 cfg.UseTokenGroups = d.Get("use_token_groups").(bool) 419 } 420 421 if _, ok := d.Raw["request_timeout"]; ok || !hadExisting { 422 cfg.RequestTimeout = d.Get("request_timeout").(int) 423 } 424 425 if _, ok := d.Raw["connection_timeout"]; ok || !hadExisting { 426 cfg.ConnectionTimeout = d.Get("connection_timeout").(int) 427 } 428 429 if _, ok := d.Raw["dereference_aliases"]; ok || !hadExisting { 430 cfg.DerefAliases = d.Get("dereference_aliases").(string) 431 } 432 433 if _, ok := d.Raw["max_page_size"]; ok || !hadExisting { 434 cfg.MaximumPageSize = d.Get("max_page_size").(int) 435 } 436 437 return cfg, nil 438 } 439 440 type ConfigEntry struct { 441 Url string `json:"url"` 442 UserDN string `json:"userdn"` 443 AnonymousGroupSearch bool `json:"anonymous_group_search"` 444 GroupDN string `json:"groupdn"` 445 GroupFilter string `json:"groupfilter"` 446 GroupAttr string `json:"groupattr"` 447 UPNDomain string `json:"upndomain"` 448 UsernameAsAlias bool `json:"username_as_alias"` 449 UserFilter string `json:"userfilter"` 450 UserAttr string `json:"userattr"` 451 Certificate string `json:"certificate"` 452 InsecureTLS bool `json:"insecure_tls"` 453 StartTLS bool `json:"starttls"` 454 BindDN string `json:"binddn"` 455 BindPassword string `json:"bindpass"` 456 DenyNullBind bool `json:"deny_null_bind"` 457 DiscoverDN bool `json:"discoverdn"` 458 TLSMinVersion string `json:"tls_min_version"` 459 TLSMaxVersion string `json:"tls_max_version"` 460 UseTokenGroups bool `json:"use_token_groups"` 461 UsePre111GroupCNBehavior *bool `json:"use_pre111_group_cn_behavior"` 462 RequestTimeout int `json:"request_timeout"` 463 ConnectionTimeout int `json:"connection_timeout"` // deprecated: use RequestTimeout 464 DerefAliases string `json:"dereference_aliases"` 465 MaximumPageSize int `json:"max_page_size"` 466 467 // These json tags deviate from snake case because there was a past issue 468 // where the tag was being ignored, causing it to be jsonified as "CaseSensitiveNames", etc. 469 // To continue reading in users' previously stored values, 470 // we chose to carry that forward. 471 CaseSensitiveNames *bool `json:"CaseSensitiveNames,omitempty"` 472 ClientTLSCert string `json:"ClientTLSCert"` 473 ClientTLSKey string `json:"ClientTLSKey"` 474 } 475 476 func (c *ConfigEntry) Map() map[string]interface{} { 477 m := c.PasswordlessMap() 478 m["bindpass"] = c.BindPassword 479 return m 480 } 481 482 func (c *ConfigEntry) PasswordlessMap() map[string]interface{} { 483 m := map[string]interface{}{ 484 "url": c.Url, 485 "userdn": c.UserDN, 486 "groupdn": c.GroupDN, 487 "groupfilter": c.GroupFilter, 488 "groupattr": c.GroupAttr, 489 "userfilter": c.UserFilter, 490 "upndomain": c.UPNDomain, 491 "userattr": c.UserAttr, 492 "certificate": c.Certificate, 493 "insecure_tls": c.InsecureTLS, 494 "starttls": c.StartTLS, 495 "binddn": c.BindDN, 496 "deny_null_bind": c.DenyNullBind, 497 "discoverdn": c.DiscoverDN, 498 "tls_min_version": c.TLSMinVersion, 499 "tls_max_version": c.TLSMaxVersion, 500 "use_token_groups": c.UseTokenGroups, 501 "anonymous_group_search": c.AnonymousGroupSearch, 502 "request_timeout": c.RequestTimeout, 503 "connection_timeout": c.ConnectionTimeout, 504 "username_as_alias": c.UsernameAsAlias, 505 "dereference_aliases": c.DerefAliases, 506 "max_page_size": c.MaximumPageSize, 507 } 508 if c.CaseSensitiveNames != nil { 509 m["case_sensitive_names"] = *c.CaseSensitiveNames 510 } 511 if c.UsePre111GroupCNBehavior != nil { 512 m["use_pre111_group_cn_behavior"] = *c.UsePre111GroupCNBehavior 513 } 514 return m 515 } 516 517 func validateCertificate(pemBlock []byte) error { 518 block, _ := pem.Decode([]byte(pemBlock)) 519 if block == nil || block.Type != "CERTIFICATE" { 520 return errors.New("failed to decode PEM block in the certificate") 521 } 522 _, err := x509.ParseCertificate(block.Bytes) 523 if err != nil { 524 return fmt.Errorf("failed to parse certificate %s", err.Error()) 525 } 526 return nil 527 } 528 529 func (c *ConfigEntry) Validate() error { 530 if len(c.Url) == 0 { 531 return errors.New("at least one url must be provided") 532 } 533 // Note: This logic is driven by the logic in GetUserBindDN. 534 // If updating this, please also update the logic there. 535 if !c.DiscoverDN && (c.BindDN == "" || c.BindPassword == "") && c.UPNDomain == "" && c.UserDN == "" { 536 return errors.New("cannot derive UserBindDN") 537 } 538 tlsMinVersion, ok := tlsutil.TLSLookup[c.TLSMinVersion] 539 if !ok { 540 return errors.New("invalid 'tls_min_version' in config") 541 } 542 tlsMaxVersion, ok := tlsutil.TLSLookup[c.TLSMaxVersion] 543 if !ok { 544 return errors.New("invalid 'tls_max_version' in config") 545 } 546 if tlsMaxVersion < tlsMinVersion { 547 return errors.New("'tls_max_version' must be greater than or equal to 'tls_min_version'") 548 } 549 if c.Certificate != "" { 550 if err := validateCertificate([]byte(c.Certificate)); err != nil { 551 return errwrap.Wrapf("failed to parse server tls cert: {{err}}", err) 552 } 553 } 554 if c.ClientTLSCert != "" && c.ClientTLSKey != "" { 555 if _, err := tls.X509KeyPair([]byte(c.ClientTLSCert), []byte(c.ClientTLSKey)); err != nil { 556 return errwrap.Wrapf("failed to parse client X509 key pair: {{err}}", err) 557 } 558 } 559 return nil 560 } 561 562 func ConvertConfig(cfg *ConfigEntry) *capldap.ClientConfig { 563 // cap/ldap doesn't have a notion of connection_timeout, and uses a single timeout value for 564 // both the net.Dialer and ldap connection timeout. 565 // So take the smaller of the two values and use that as the timeout value. 566 minTimeout := min(cfg.ConnectionTimeout, cfg.RequestTimeout) 567 urls := strings.Split(cfg.Url, ",") 568 config := &capldap.ClientConfig{ 569 URLs: urls, 570 UserDN: cfg.UserDN, 571 AnonymousGroupSearch: cfg.AnonymousGroupSearch, 572 GroupDN: cfg.GroupDN, 573 GroupFilter: cfg.GroupFilter, 574 GroupAttr: cfg.GroupAttr, 575 UPNDomain: cfg.UPNDomain, 576 UserFilter: cfg.UserFilter, 577 UserAttr: cfg.UserAttr, 578 ClientTLSCert: cfg.ClientTLSCert, 579 ClientTLSKey: cfg.ClientTLSKey, 580 InsecureTLS: cfg.InsecureTLS, 581 StartTLS: cfg.StartTLS, 582 BindDN: cfg.BindDN, 583 BindPassword: cfg.BindPassword, 584 AllowEmptyPasswordBinds: !cfg.DenyNullBind, 585 DiscoverDN: cfg.DiscoverDN, 586 TLSMinVersion: cfg.TLSMinVersion, 587 TLSMaxVersion: cfg.TLSMaxVersion, 588 UseTokenGroups: cfg.UseTokenGroups, 589 RequestTimeout: minTimeout, 590 IncludeUserAttributes: true, 591 ExcludedUserAttributes: nil, 592 IncludeUserGroups: true, 593 LowerUserAttributeKeys: true, 594 AllowEmptyAnonymousGroupSearch: true, 595 MaximumPageSize: cfg.MaximumPageSize, 596 DerefAliases: cfg.DerefAliases, 597 DeprecatedVaultPre111GroupCNBehavior: cfg.UsePre111GroupCNBehavior, 598 } 599 600 if cfg.Certificate != "" { 601 config.Certificates = []string{cfg.Certificate} 602 } 603 604 return config 605 } 606 607 func min(a, b int) int { 608 if a < b { 609 return a 610 } 611 return b 612 }