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>
This commit is contained in:
Avinash Nigam 2021-04-21 09:02:07 -07:00 committed by GitHub
parent 4336dae3b5
commit f39d68d1fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 175 additions and 17 deletions

1
go.mod
View File

@ -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

5
go.sum
View File

@ -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=

View File

@ -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 = ''<Monitoring_VM_Name>'')
BEGIN
DROP USER [<Monitoring_VM_Name>]
END')
EXECUTE ('CREATE USER [<Monitoring_VM_Name>] FROM EXTERNAL PROVIDER')
EXECUTE ('GRANT VIEW DATABASE STATE TO [<Monitoring_VM_Name>]')
```
- 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=<Azure_SQL_Server_Name>.database.windows.net;Port=1433;Database=<Azure_SQL_Database_Name>;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.

View File

@ -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}}

View File

@ -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},