Skip to content
This repository has been archived by the owner on Sep 1, 2024. It is now read-only.

Add TLS/StartTLS support #17

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,13 @@ endif

compile = GOOS=$1 GOARCH=amd64 go build -ldflags "${LDFLAGS}" -o target/openldap_exporter-$1 ./cmd/openldap_exporter

.PHONY: build
build: target
.PHONY: build-linux
build-linux: target
$(call compile,linux)

.PHONY: build-darwin
build-darwin: target
$(call compile,darwin)

.PHONY: build
build: target build-linux build-darwin
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@ VERSION:
GLOBAL OPTIONS:
--promAddr value Bind address for Prometheus HTTP metrics server (default: ":9330") [$PROM_ADDR]
--metrPath value Path on which to expose Prometheus metrics (default: "/metrics") [$METRICS_PATH]
--ldapNet value Network of OpenLDAP server (default: "tcp") [$LDAP_NET]
--ldapAddr value Address and port of OpenLDAP server (default: "localhost:389") [$LDAP_ADDR]
--ldapAddr value Address of OpenLDAP server (default "ldap://localhost") [$LDAP_ADDR]
--ldapUser value OpenLDAP bind username (optional) [$LDAP_USER]
--ldapPass value OpenLDAP bind password (optional) [$LDAP_PASS]
--ldapSkipInsecure OpenLDAP Skip TLS verify (default: false) [$LDAP_SKIP_TLS_VERIFY]
--ldapUseStartTLS Use start TLS (optional)
--ldapCACrt string Path to CA certificate for LDAPS (optional)
--interval value Scrape interval (default: 30s) [$INTERVAL]
--webCfgFile FILE Prometheus metrics web config FILE (optional) [$WEB_CFG_FILE]
--jsonLog Output logs in JSON format (default: false) [$JSON_LOG]
Expand All @@ -121,8 +123,15 @@ ldapPass: "sekret"

NOTES:

* `ldapNet` allows you to configure `tcp` or `unix` socket connections to your co-located OpenLDAP server.
* `webCfgFile` can be used to provide authentication and TLS configuration for the [prometheus web exporter](https://github.com/prometheus/exporter-toolkit/tree/master/web).
* `ldapAddr` supports `ldaps://` (default port is `636`), `ldap://` (default port is `389`) and `ldapi://` scheme uri's. (defaults to ldap:// scheme)
using the LDAPS scheme will open a connection using TLS. Examples:
- `ldapi:///var/run/ldapi`
- `ldaps://ldap.host.net:666`
- `ldap://ldap.host.net`
* Use `ldapUseStartTLS` to use StartTLS for ldap:// scheme.
* Use `ldapSkipInsecure` to skip TLS verify.
* `ldapCACrt` if the ldap server uses a custom CA certificate, add the path to the public CA Cert in PEM format

## Build

Expand Down
58 changes: 46 additions & 12 deletions cmd/openldap_exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import (

const (
promAddr = "promAddr"
ldapNet = "ldapNet"
ldapAddr = "ldapAddr"
ldapCACrt = "ldapCACrt"
ldapUser = "ldapUser"
ldapPass = "ldapPass"
ldapSkipInsecure = "ldapSkipInsecure"
ldapUseStartTLS = "ldapUseStartTLS"
interval = "interval"
metrics = "metrPath"
jsonLog = "jsonLog"
Expand All @@ -43,18 +45,17 @@ func main() {
Usage: "Path on which to expose Prometheus metrics",
EnvVars: []string{"METRICS_PATH"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ldapNet,
Value: "tcp",
Usage: "Network of OpenLDAP server",
EnvVars: []string{"LDAP_NET"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ldapAddr,
Value: "localhost:389",
Value: "ldap://localhost",
Usage: "Address and port of OpenLDAP server",
EnvVars: []string{"LDAP_ADDR"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ldapCACrt,
Usage: "Path to CA certificate for LDAPS (optional)",
EnvVars: []string{"LDAP_CA_CRT"},
}),
altsrc.NewStringFlag(&cli.StringFlag{
Name: ldapUser,
Usage: "OpenLDAP bind username (optional)",
Expand All @@ -65,6 +66,18 @@ func main() {
Usage: "OpenLDAP bind password (optional)",
EnvVars: []string{"LDAP_PASS"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ldapSkipInsecure,
Value: false,
Usage: "OpenLDAP Skip TLS verify (default=false)",
EnvVars: []string{"LDAP_SKIP_TLS_VERIFY"},
}),
altsrc.NewBoolFlag(&cli.BoolFlag{
Name: ldapUseStartTLS,
Value: false,
Usage: "Use start TLS (optional)",
EnvVars: []string{"LDAP_USE_STARTTLS"},
}),
altsrc.NewDurationFlag(&cli.DurationFlag{
Name: interval,
Value: 30 * time.Second,
Expand Down Expand Up @@ -124,17 +137,38 @@ func runMain(c *cli.Context) error {
}
log.Info("service starting")

config := exporter.NewLDAPConfig()

// Process Address & TLS options
err := config.ProcessTLSoptions(c.String(ldapAddr),c.Bool(ldapUseStartTLS),c.Bool(ldapSkipInsecure))
if err != nil {
log.Println("Error parsing ldap address: ", err.Error())
os.Exit(1)
}

/** Load Certificate if given, and panic on error **/
if c.String(ldapCACrt) != "" {
err = config.LoadCACert(c.String(ldapCACrt))
if err != nil {
log.Println("Error loading CA certificate file: ", err.Error())
os.Exit(1)
} else {
log.Println("Successfully loaded CA cert file:", c.String(ldapCACrt))
}
}

config.Username = c.String(ldapUser)
config.Password = c.String(ldapPass)


server := exporter.NewMetricsServer(
c.String(promAddr),
c.String(metrics),
c.String(webCfgFile),
)

scraper := &exporter.Scraper{
Net: c.String(ldapNet),
Addr: c.String(ldapAddr),
User: c.String(ldapUser),
Pass: c.String(ldapPass),
LDAPConfig: config,
Tick: c.Duration(interval),
Sync: c.StringSlice(replicationObject),
}
Expand Down
176 changes: 162 additions & 14 deletions scraper.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package openldap_exporter

import (
"crypto/tls"
"crypto/x509"
"errors"
"context"
"fmt"
"io/ioutil"
"net/url"
"os"
"strconv"
"strings"
"time"
"regexp"

"github.com/prometheus/client_golang/prometheus"
log "github.com/sirupsen/logrus"
Expand All @@ -27,6 +34,10 @@ const (

monitorReplicationFilter = "contextCSN"
monitorReplication = "monitorReplication"

SchemeLDAPS = "ldaps"
SchemeLDAP = "ldap"
SchemeLDAPI = "ldapi"
)

type query struct {
Expand Down Expand Up @@ -178,15 +189,121 @@ func setReplicationValue(entries []*ldap.Entry, q *query) {
}
}

type LDAPConfig struct {
UseTLS bool
UseStartTLS bool
Scheme string
Addr string
Host string
Port string
Protocol string
Username string
Password string
TLSConfig tls.Config
}

type Scraper struct {
Net string
Addr string
User string
Pass string
Tick time.Duration
LdapSync []string
log log.FieldLogger
Sync []string
LDAPConfig LDAPConfig
Tick time.Duration
log log.FieldLogger
Sync []string
}


func (config *LDAPConfig) ProcessTLSoptions(addr string, useStartTLS bool, skipInsecure bool) error {

var u *url.URL

u, err := url.Parse(addr)
if (err != nil) {
// Well, so far the easy way....
u = &url.URL{}
}

if u.Host == "" {
if strings.HasPrefix(addr, SchemeLDAPI) {
u.Scheme = SchemeLDAPI
u.Host, _ = url.QueryUnescape(strings.Replace(addr, SchemeLDAPI+"://", "", 1))
} else if strings.HasPrefix(addr, SchemeLDAPS) {
u.Scheme = SchemeLDAPS
u.Host = strings.Replace(addr, SchemeLDAPS+"://", "", 1)
} else {
u.Scheme = SchemeLDAP
u.Host = strings.Replace(addr, SchemeLDAP+"://", "", 1)
}
}

config.Addr = u.Host
config.Scheme = u.Scheme
config.Host = u.Hostname()

r, _ := regexp.Compile(":[0-9]+")
if u.Scheme == SchemeLDAPS {
config.UseTLS = true
if ! r.MatchString(config.Addr){
config.Port = "636"
config.Addr += ":" + config.Port
}
} else if u.Scheme == SchemeLDAP {
config.UseTLS = false
if ! r.MatchString(config.Addr){
config.Port = "389"
config.Addr += ":" + config.Port
}
} else if u.Scheme == SchemeLDAPI {
config.Protocol = "unix"
} else {
return errors.New(u.Scheme + " is not a scheme i understand, refusing to continue")
}

config.TLSConfig.InsecureSkipVerify = skipInsecure
config.TLSConfig.ServerName = config.Host
if ! config.UseTLS {
// useStartTLS only relevant if not using TLS
config.UseStartTLS = useStartTLS
}

return nil
}

func (config *LDAPConfig) LoadCACert(cafile string) error {

if _, err := os.Stat(cafile); os.IsNotExist(err) {
return errors.New("CA Certificate file does not exists")
}

cert, err := ioutil.ReadFile(cafile)

if err != nil {
return errors.New("CA Certificate file is not readable")
}

config.TLSConfig.RootCAs = x509.NewCertPool()

ok := config.TLSConfig.RootCAs.AppendCertsFromPEM(cert)

if ok == false {
return errors.New("Could not parse CA")
}

return nil

}

func NewLDAPConfig() LDAPConfig {

conf := LDAPConfig{}

conf.Scheme = SchemeLDAP
conf.Host = "localhost"
conf.Port = "389"
conf.Addr = conf.Host + ":" + conf.Port
conf.Protocol = "tcp"
conf.UseTLS = false
conf.UseStartTLS = false
conf.TLSConfig = tls.Config{}

return conf
}

func (s *Scraper) addReplicationQueries() {
Expand All @@ -206,8 +323,18 @@ func (s *Scraper) addReplicationQueries() {
func (s *Scraper) Start(ctx context.Context) error {
s.log = log.WithField("component", "scraper")
s.addReplicationQueries()
address := fmt.Sprintf("%s://%s", s.Net, s.Addr)
s.log.WithField("addr", address).Info("starting monitor loop")
security := "None"
s.log.Info("SECURITY: " + security)
if s.LDAPConfig.UseTLS {
security = "TLS"
} else if s.LDAPConfig.UseStartTLS {
security = "StartTLS"
}
if s.LDAPConfig.TLSConfig.InsecureSkipVerify {
security += "/InsecureSkipVerify"
}
address := s.LDAPConfig.Scheme + "://" + s.LDAPConfig.Addr
s.log.WithField("addr", address).WithField("security", security).Info("starting monitor loop")
ticker := time.NewTicker(s.Tick)
defer ticker.Stop()
for {
Expand All @@ -229,15 +356,36 @@ func (s *Scraper) runOnce() {
}

func (s *Scraper) scrape() bool {
conn, err := ldap.Dial(s.Net, s.Addr)
var conn *ldap.Conn
var err error

if s.LDAPConfig.UseTLS {
conn, err = ldap.DialTLS(s.LDAPConfig.Protocol, s.LDAPConfig.Addr, &s.LDAPConfig.TLSConfig)
} else {
conn, err = ldap.Dial(s.LDAPConfig.Protocol, s.LDAPConfig.Addr)
if err != nil {
s.log.WithError(err).Error("dial failed")
return false
}

if s.LDAPConfig.UseStartTLS {
err = conn.StartTLS(&s.LDAPConfig.TLSConfig)
if err != nil {
s.log.WithError(err).Error("StartTLS failed")
return false
}
}
}

if err != nil {
s.log.WithError(err).Error("dial failed")
return false
}
defer conn.Close()

if s.User != "" && s.Pass != "" {
err = conn.Bind(s.User, s.Pass)
defer conn.Close()

if s.LDAPConfig.Username != "" && s.LDAPConfig.Password != "" {
err = conn.Bind(s.LDAPConfig.Username, s.LDAPConfig.Password)
if err != nil {
s.log.WithError(err).Error("bind failed")
return false
Expand Down