From f39d68d1faf4f688d24657b589ba01c9ffb93a18 Mon Sep 17 00:00:00 2001 From: Avinash Nigam <56562150+avinash-nigam@users.noreply.github.com> Date: Wed, 21 Apr 2021 09:02:07 -0700 Subject: [PATCH] SQL Server input plugin - Enable Azure Active Directory (AAD) authentication support (#8822) ### Required for all PRs: - [ ] Associated README.md updated. - [ ] Has appropriate unit tests. Associated to feature request - [Azure Active Directory (AAD) authentication support in SQL Server input plugin](https://github.com/influxdata/telegraf/issues/8808#issue-801695311) Co-authored-by: Sven Rebhan <36194019+srebhan@users.noreply.github.com> --- go.mod | 1 + go.sum | 5 +- plugins/inputs/sqlserver/README.md | 34 ++++- plugins/inputs/sqlserver/sqlserver.go | 148 +++++++++++++++++++-- plugins/inputs/sqlserver/sqlserver_test.go | 4 +- 5 files changed, 175 insertions(+), 17 deletions(-) diff --git a/go.mod b/go.mod index 523e4fbdf..57ee3c129 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/Azure/azure-event-hubs-go/v3 v3.2.0 github.com/Azure/azure-storage-queue-go v0.0.0-20181215014128-6ed74e755687 github.com/Azure/go-autorest/autorest v0.11.17 + github.com/Azure/go-autorest/autorest/adal v0.9.10 github.com/Azure/go-autorest/autorest/azure/auth v0.5.6 github.com/BurntSushi/toml v0.3.1 github.com/Mellanox/rdmamap v0.0.0-20191106181932-7c3c4763a6ee diff --git a/go.sum b/go.sum index 795772ffc..f29d3c36b 100644 --- a/go.sum +++ b/go.sum @@ -352,6 +352,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.10.0 h1:dXFJfIHVvUcpSgDOV+Ne6t7jXri8Tfv2uOLHUZ2XNuo= github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -518,6 +519,7 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -895,6 +897,7 @@ github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go github.com/opentracing-contrib/go-stdlib v1.0.0/go.mod h1:qtI1ogk+2JhVPIXVc6q+NHziSmy2W5GbdQZFUHADCBU= github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.0.3-0.20180606204148-bd9c31933947/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= @@ -1039,7 +1042,6 @@ github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrf github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v1.6.4-0.20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs= github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= @@ -1403,7 +1405,6 @@ golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= diff --git a/plugins/inputs/sqlserver/README.md b/plugins/inputs/sqlserver/README.md index ee2dc52c3..d5ad22ee7 100644 --- a/plugins/inputs/sqlserver/README.md +++ b/plugins/inputs/sqlserver/README.md @@ -172,11 +172,39 @@ GO ## - VolumeSpace ## - PerformanceMetrics - - - ``` +### Support for Azure Active Directory (AAD) authentication using [Managed Identity](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview) + +Azure SQL Database supports 2 main methods of authentication: [SQL authentication and AAD authentication](https://docs.microsoft.com/en-us/azure/azure-sql/database/security-overview#authentication). The recommended practice is to [use AAD authentication when possible](https://docs.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-overview). + +AAD is a more modern authentication protocol, allows for easier credential/role management, and can eliminate the need to include passwords in a connection string. + +To enable support for AAD authentication, we leverage the existing AAD authentication support in the [SQL Server driver for Go](https://github.com/denisenkom/go-mssqldb#azure-active-directory-authentication---preview) + +#### How to use AAD Auth with MSI + +- Configure "system-assigned managed identity" for Azure resources on the Monitoring VM (the VM that'd connect to the SQL server/database) [using the Azure portal](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/qs-configure-portal-windows-vm). +- On the database being monitored, create/update a USER with the name of the Monitoring VM as the principal using the below script. This might require allow-listing the client machine's IP address (from where the below SQL script is being run) on the SQL Server resource. +```sql +EXECUTE ('IF EXISTS(SELECT * FROM sys.database_principals WHERE name = '''') + BEGIN + DROP USER [] + END') +EXECUTE ('CREATE USER [] FROM EXTERNAL PROVIDER') +EXECUTE ('GRANT VIEW DATABASE STATE TO []') +``` +- On the SQL Server resource of the database(s) being monitored, go to "Firewalls and Virtual Networks" tab and allowlist the monitoring VM IP address. +- On the Monitoring VM, update the telegraf config file with the database connection string in the following format. Please note AAD based auth is currently only supported for Azure SQL Database and Azure SQL Managed Instance (but not for SQL Server), as described [here](https://docs.microsoft.com/en-us/azure/azure-sql/database/security-overview#authentication). +- On the Monitoring VM, update the telegraf config file with the database connection string in the following format. +- On the Monitoring VM, update the telegraf config file with the database connection string in the following format. The connection string only provides the server and database name, but no password (since the VM's system-assigned managed identity would be used for authentication). +```toml + servers = [ + "Server=.database.windows.net;Port=1433;Database=;app name=telegraf;log=1;", + ] +``` +- Please note AAD based auth is currently only supported for Azure SQL Database and Azure SQL Managed Instance (but not for SQL Server), as described [here](https://docs.microsoft.com/en-us/azure/azure-sql/database/security-overview#authentication). + ### Metrics: To provide backwards compatibility, this plugin support two versions of metrics queries. diff --git a/plugins/inputs/sqlserver/sqlserver.go b/plugins/inputs/sqlserver/sqlserver.go index db499a747..7da1218c0 100644 --- a/plugins/inputs/sqlserver/sqlserver.go +++ b/plugins/inputs/sqlserver/sqlserver.go @@ -2,12 +2,15 @@ package sqlserver import ( "database/sql" + "errors" "fmt" "log" + "regexp" "sync" "time" - _ "github.com/denisenkom/go-mssqldb" // go-mssqldb initialization + "github.com/Azure/go-autorest/autorest/adal" + mssql "github.com/denisenkom/go-mssqldb" "github.com/influxdata/telegraf" "github.com/influxdata/telegraf/filter" "github.com/influxdata/telegraf/plugins/inputs" @@ -24,6 +27,8 @@ type SQLServer struct { HealthMetric bool `toml:"health_metric"` pools []*sql.DB queries MapQuery + adalToken *adal.Token + muCacheLock sync.RWMutex } // Query struct @@ -60,6 +65,9 @@ const ( healthMetricDatabaseType = "database_type" ) +// resource id for Azure SQL Database +const sqlAzureResourceID = "https://database.windows.net/" + const sampleConfig = ` ## Specify instances to monitor with a list of connection strings. ## All connection parameters are optional. @@ -272,15 +280,48 @@ func (s *SQLServer) Start(acc telegraf.Accumulator) error { return err } - if len(s.Servers) == 0 { - s.Servers = append(s.Servers, defaultServer) - } + // initialize mutual exclusion lock + s.muCacheLock = sync.RWMutex{} for _, serv := range s.Servers { - pool, err := sql.Open("mssql", serv) - if err != nil { - acc.AddError(err) - return err + var pool *sql.DB + + // setup connection based on authentication + rx := regexp.MustCompile(`\b(?:(Password=((?:&(?:[a-z]+|#[0-9]+);|[^;]){0,})))\b`) + + // when password is provided in connection string, use SQL auth + if rx.MatchString(serv) { + var err error + pool, err = sql.Open("mssql", serv) + + if err != nil { + acc.AddError(err) + continue + } + } else { + // otherwise assume AAD Auth with system-assigned managed identity (MSI) + + // AAD Auth is only supported for Azure SQL Database or Azure SQL Managed Instance + if s.DatabaseType == "SQLServer" { + err := errors.New("database connection failed : AAD auth is not supported for SQL VM i.e. DatabaseType=SQLServer") + acc.AddError(err) + continue + } + + // get token from in-memory cache variable or from Azure Active Directory + tokenProvider, err := s.getTokenProvider() + if err != nil { + acc.AddError(fmt.Errorf("error creating AAD token provider for system assigned Azure managed identity : %s", err.Error())) + continue + } + + connector, err := mssql.NewAccessTokenConnector(serv, tokenProvider) + if err != nil { + acc.AddError(fmt.Errorf("error creating the SQL connector : %s", err.Error())) + continue + } + + pool = sql.OpenDB(connector) } s.pools = append(s.pools, pool) @@ -300,8 +341,7 @@ func (s *SQLServer) gatherServer(pool *sql.DB, query Query, acc telegraf.Accumul // execute query rows, err := pool.Query(query.Script) if err != nil { - return fmt.Errorf("Script %s failed: %w", query.ScriptName, err) - //return err + return fmt.Errorf("script %s failed: %w", query.ScriptName, err) } defer rows.Close() @@ -423,6 +463,94 @@ func (s *SQLServer) Init() error { return nil } +// Get Token Provider by loading cached token or refreshed token +func (s *SQLServer) getTokenProvider() (func() (string, error), error) { + var tokenString string + + // load token + s.muCacheLock.RLock() + token, err := s.loadToken() + s.muCacheLock.RUnlock() + + // if there's error while loading token or found an expired token, refresh token and save it + if err != nil || token.IsExpired() { + // refresh token within a write-lock + s.muCacheLock.Lock() + defer s.muCacheLock.Unlock() + + // load token again, in case it's been refreshed by another thread + token, err = s.loadToken() + + // check loaded token's error/validity, then refresh/save token + if err != nil || token.IsExpired() { + // get new token + spt, err := s.refreshToken() + if err != nil { + return nil, err + } + + // use the refreshed token + tokenString = spt.OAuthToken() + } else { + // use locally cached token + tokenString = token.OAuthToken() + } + } else { + // use locally cached token + tokenString = token.OAuthToken() + } + + // return acquired token + return func() (string, error) { + return tokenString, nil + }, nil +} + +// Load token from in-mem cache +func (s *SQLServer) loadToken() (*adal.Token, error) { + // This method currently does a simplistic task of reading a from variable (in-mem cache), + // however it's been structured here to allow extending the cache mechanism to a different approach in future + + if s.adalToken == nil { + return nil, fmt.Errorf("token is nil or failed to load existing token") + } + + return s.adalToken, nil +} + +// Refresh token for the resource, and save to in-mem cache +func (s *SQLServer) refreshToken() (*adal.Token, error) { + // get MSI endpoint to get a token + msiEndpoint, err := adal.GetMSIVMEndpoint() + if err != nil { + return nil, err + } + + // get new token for the resource id + spt, err := adal.NewServicePrincipalTokenFromMSI(msiEndpoint, sqlAzureResourceID) + if err != nil { + return nil, err + } + + // ensure token is fresh + if err := spt.EnsureFresh(); err != nil { + return nil, err + } + + // save token to local in-mem cache + s.adalToken = &adal.Token{ + AccessToken: spt.Token().AccessToken, + RefreshToken: spt.Token().RefreshToken, + ExpiresIn: spt.Token().ExpiresIn, + ExpiresOn: spt.Token().ExpiresOn, + NotBefore: spt.Token().NotBefore, + Resource: spt.Token().Resource, + Type: spt.Token().Type, + } + + return s.adalToken, nil +} + func init() { inputs.Add("sqlserver", func() telegraf.Input { return &SQLServer{Servers: []string{defaultServer}} diff --git a/plugins/inputs/sqlserver/sqlserver_test.go b/plugins/inputs/sqlserver/sqlserver_test.go index 580bfe5ee..3d1ddd309 100644 --- a/plugins/inputs/sqlserver/sqlserver_test.go +++ b/plugins/inputs/sqlserver/sqlserver_test.go @@ -184,8 +184,8 @@ func TestSqlServer_MultipleInstanceWithHealthMetricIntegration(t *testing.T) { } func TestSqlServer_HealthMetric(t *testing.T) { - fakeServer1 := "localhost\\fakeinstance1;Database=fakedb1" - fakeServer2 := "localhost\\fakeinstance2;Database=fakedb2" + fakeServer1 := "localhost\\fakeinstance1;Database=fakedb1;Password=ABCabc01;" + fakeServer2 := "localhost\\fakeinstance2;Database=fakedb2;Password=ABCabc01;" s1 := &SQLServer{ Servers: []string{fakeServer1, fakeServer2},