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:
parent
4336dae3b5
commit
f39d68d1fa
1
go.mod
1
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
|
||||
|
|
|
|||
5
go.sum
5
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=
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
Loading…
Reference in New Issue