From 0d885e231e5547320264c90a4a28654c87912ecc Mon Sep 17 00:00:00 2001 From: Gabriel Corado Date: Tue, 2 May 2023 14:31:16 -0300 Subject: [PATCH] SQL Server PKINIT configuration script (#24521) * feat(web): add powershell configure script for sqlserver * chore(web): fix license * Update lib/web/databases.go Co-authored-by: Isaiah Becker-Mayer * refactor(web): rename endpoint --------- Co-authored-by: Isaiah Becker-Mayer --- lib/web/apiserver.go | 1 + lib/web/databases.go | 82 +++++++++++ lib/web/databases_test.go | 66 +++++++++ lib/web/scripts/database.go | 46 ++++++ .../database/sqlserver/configure-ad.ps1 | 131 ++++++++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 lib/web/scripts/database.go create mode 100644 lib/web/scripts/database/sqlserver/configure-ad.ps1 diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 1e8d4f8508b..2f3d2a824f2 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -647,6 +647,7 @@ func (h *Handler) bindDefaultEndpoints() { h.PUT("/webapi/sites/:site/databases/:database", h.WithClusterAuth(h.handleDatabaseUpdate)) h.GET("/webapi/sites/:site/databases/:database", h.WithClusterAuth(h.clusterDatabaseGet)) h.GET("/webapi/sites/:site/databases/:database/iam/policy", h.WithClusterAuth(h.handleDatabaseGetIAMPolicy)) + h.GET("/webapi/scripts/databases/configure/sqlserver/:token/configure-ad.ps1", httplib.MakeHandler(h.sqlServerConfigureADScriptHandle)) // DatabaseService handlers h.GET("/webapi/sites/:site/databaseservices", h.WithClusterAuth(h.clusterDatabaseServicesList)) diff --git a/lib/web/databases.go b/lib/web/databases.go index d96531d7215..e904f405b54 100644 --- a/lib/web/databases.go +++ b/lib/web/databases.go @@ -18,7 +18,10 @@ package web import ( "context" + "crypto/sha1" + "encoding/base64" "encoding/json" + "encoding/pem" "fmt" "net/http" @@ -32,6 +35,7 @@ import ( "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/reversetunnel" dbiam "github.com/gravitational/teleport/lib/srv/db/common/iam" + "github.com/gravitational/teleport/lib/web/scripts" "github.com/gravitational/teleport/lib/web/ui" ) @@ -280,6 +284,76 @@ func (h *Handler) handleDatabaseGetIAMPolicy(w http.ResponseWriter, r *http.Requ } } +func (h *Handler) sqlServerConfigureADScriptHandle(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + tokenStr := p.ByName("token") + if tokenStr == "" { + return "", trace.BadParameter("invalid token") + } + + dbAddress := r.URL.Query().Get("uri") + if dbAddress == "" { + return "", trace.BadParameter("invalid database address") + } + + // verify that the token exists + _, err := h.GetProxyClient().GetToken(r.Context(), tokenStr) + if err != nil { + return "", trace.BadParameter("invalid token") + } + + proxyServers, err := h.GetProxyClient().GetProxies() + if err != nil { + return "", trace.Wrap(err) + } + + if len(proxyServers) == 0 { + return "", trace.NotFound("no proxy servers found") + } + + clusterName, err := h.GetProxyClient().GetDomainName(r.Context()) + if err != nil { + return nil, trace.Wrap(err) + } + + certAuthority, err := h.GetProxyClient().GetCertAuthority( + r.Context(), + types.CertAuthID{Type: types.DatabaseCA, DomainName: clusterName}, + false, + ) + if err != nil { + return nil, trace.Wrap(err) + } + + caCRL, err := h.GetProxyClient().GenerateCertAuthorityCRL(r.Context(), types.DatabaseCA) + if err != nil { + return nil, trace.Wrap(err) + } + + if len(certAuthority.GetActiveKeys().TLS) != 1 { + return nil, trace.BadParameter("expected one TLS key pair, got %v", len(certAuthority.GetActiveKeys().TLS)) + } + + keyPair := certAuthority.GetActiveKeys().TLS[0] + block, _ := pem.Decode(keyPair.Cert) + if block == nil { + return nil, trace.BadParameter("no PEM data in CA data") + } + + httplib.SetScriptHeaders(w.Header()) + w.WriteHeader(http.StatusOK) + err = scripts.DatabaseAccessSQLServerConfigureScript.Execute(w, scripts.DatabaseAccessSQLServerConfigureParams{ + CACertPEM: string(keyPair.Cert), + CACertSHA1: fmt.Sprintf("%X", sha1.Sum(block.Bytes)), + CACertBase64: base64.StdEncoding.EncodeToString(createCertificateBlob(block.Bytes)), + CRLPEM: string(encodeCRLPEM(caCRL)), + ProxyPublicAddr: proxyServers[0].GetPublicAddr(), + ProvisionToken: tokenStr, + DBAddress: dbAddress, + }) + + return nil, trace.Wrap(err) +} + // fetchDatabaseWithName fetch a database with provided database name. func fetchDatabaseWithName(ctx context.Context, clt resourcesAPIGetter, r *http.Request, databaseName string) (types.Database, error) { resp, err := clt.ListResources(ctx, proto.ListResourcesRequest{ @@ -338,3 +412,11 @@ func getNewDatabaseResource(req createDatabaseRequest) (*types.DatabaseV3, error return database, nil } + +// encodeCRLPEM takes DER encoded CRL and encodes into PEM. +func encodeCRLPEM(contents []byte) []byte { + return pem.EncodeToMemory(&pem.Block{ + Type: "X509 CRL", + Bytes: contents, + }) +} diff --git a/lib/web/databases_test.go b/lib/web/databases_test.go index 2ba77afc4a1..8d920ad42cd 100644 --- a/lib/web/databases_test.go +++ b/lib/web/databases_test.go @@ -20,7 +20,9 @@ import ( "context" "encoding/json" "net/http" + "net/url" "testing" + "time" "github.com/google/uuid" "github.com/gravitational/roundtrip" @@ -28,9 +30,11 @@ import ( "github.com/stretchr/testify/require" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth" awslib "github.com/gravitational/teleport/lib/cloud/aws" "github.com/gravitational/teleport/lib/services" dbiam "github.com/gravitational/teleport/lib/srv/db/common/iam" + "github.com/gravitational/teleport/lib/utils" "github.com/gravitational/teleport/lib/web/ui" ) @@ -334,6 +338,56 @@ func TestHandleDatabaseServicesGet(t *testing.T) { require.Equal(t, respResourceMatcher.Labels, &types.Labels{"env": []string{"prod"}}) } +func TestHandleSQLServerConfigureScript(t *testing.T) { + ctx := context.Background() + env := newWebPack(t, 1) + proxy := env.proxies[0] + pack := proxy.authPack(t, "user", nil /* roles */) + + for _, tc := range []struct { + desc string + uri string + assertError require.ErrorAssertionFunc + tokenFunc func(*testing.T) string + }{ + { + desc: "valid token and uri", + uri: "instance.example.teleport.dev", + tokenFunc: func(t *testing.T) string { + pt, token := generateProvisionToken(t, types.RoleDatabase, env.clock.Now().Add(time.Hour)) + require.NoError(t, env.server.Auth().CreateToken(ctx, pt)) + return token + }, + assertError: require.NoError, + }, + { + desc: "valid token and invalid uri", + uri: "", + tokenFunc: func(t *testing.T) string { + pt, token := generateProvisionToken(t, types.RoleDatabase, env.clock.Now().Add(time.Hour)) + require.NoError(t, env.server.Auth().CreateToken(ctx, pt)) + return token + }, + assertError: require.Error, + }, + { + desc: "invalid token", + uri: "instance.example.teleport.dev", + tokenFunc: func(_ *testing.T) string { return "random-token" }, + assertError: require.Error, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + _, err := pack.clt.Get( + ctx, + pack.clt.Endpoint("webapi/scripts/databases/configure/sqlserver", tc.tokenFunc(t), "configure-ad.ps1"), + url.Values{"uri": []string{tc.uri}}, + ) + tc.assertError(t, err) + }) + } +} + func mustCreateDatabaseServer(t *testing.T, db *types.DatabaseV3) types.DatabaseServer { t.Helper() @@ -367,3 +421,15 @@ func requireDatabaseIAMPolicyAWS(t *testing.T, respBody []byte, database types.D func strPtr(str string) *string { return &str } + +func generateProvisionToken(t *testing.T, role types.SystemRole, expiresAt time.Time) (types.ProvisionToken, string) { + t.Helper() + + token, err := utils.CryptoRandomHex(auth.TokenLenBytes) + require.NoError(t, err) + + pt, err := types.NewProvisionToken(token, types.SystemRoles{role}, expiresAt) + require.NoError(t, err) + + return pt, token +} diff --git a/lib/web/scripts/database.go b/lib/web/scripts/database.go new file mode 100644 index 00000000000..39e2f61a9a1 --- /dev/null +++ b/lib/web/scripts/database.go @@ -0,0 +1,46 @@ +// Copyright 2023 Gravitational, Inc +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package scripts + +import ( + _ "embed" + "text/template" +) + +//go:embed database/sqlserver/configure-ad.ps1 +var databaseAccessSQLServerConfigureScript string + +// DatabaseAccessSQLServerConfigureScript is the script that will run on Windows +// machine and configure Active Directory. +var DatabaseAccessSQLServerConfigureScript = template.Must(template.New("database-access-sqlserver-configure-ad").Parse(databaseAccessSQLServerConfigureScript)) + +// DatabaseAccessSQLServerConfigureParams is the template parameters passed to +// the configure script. +type DatabaseAccessSQLServerConfigureParams struct { + // CACertPEM PEM-encoded database CA. + CACertPEM string + // CACertSHA1 database CA SHA1 checksum. + CACertSHA1 string + // CACertBase64 base64-encoded database CA. + CACertBase64 string + // CRLPEM PEM-encoded database revocation list. + CRLPEM string + // ProxyPublicAddr Teleport proxy public address. + ProxyPublicAddr string + // ProvisionToken join token with database permission. + ProvisionToken string + // DBAddress database address URI. + DBAddress string +} diff --git a/lib/web/scripts/database/sqlserver/configure-ad.ps1 b/lib/web/scripts/database/sqlserver/configure-ad.ps1 new file mode 100644 index 00000000000..04b65bd13e2 --- /dev/null +++ b/lib/web/scripts/database/sqlserver/configure-ad.ps1 @@ -0,0 +1,131 @@ +$ErrorActionPreference = "Stop" + +$TELEPORT_CA_CERT_PEM = "{{.CACertPEM}}" +$TELEPORT_CA_CERT_SHA1 = "{{.CACertSHA1}}" +$TELEPORT_CA_CERT_BLOB_BASE64 = "{{.CACertBase64}}" +$TELEPORT_CRL_PEM = "{{.CRLPEM}}" +$TELEPORT_PROXY_PUBLIC_ADDR = "{{.ProxyPublicAddr}}" +$TELEPORT_PROVISION_TOKEN = "{{.ProvisionToken}}" + +$DB_ADDRESS = "{{.DBAddress}}" +$COMPUTER_NAME = ($DB_ADDRESS -split '\.')[0].ToLower() + +$DOMAIN_NAME=(Get-ADDomain).DNSRoot +$DOMAIN_DN=$((Get-ADDomain).DistinguishedName) + +# # Step 1: Configure GPO to enable Teleport access. +$ACCESS_GPO_NAME="Teleport DB access" +try { + $ACCESS_GPO = Get-GPO -Name $ACCESS_GPO_NAME +} +catch [System.ArgumentException] +{ + $ACCESS_GPO = New-GPO -Name $ACCESS_GPO_NAME + $ACCESS_GPO | New-GPLink -Target $DOMAIN_DN +} + +$CERT = [System.Convert]::FromBase64String($TELEPORT_CA_CERT_BLOB_BASE64) +Set-GPRegistryValue -Name $ACCESS_GPO_NAME -Key "HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\SystemCertificates\Root\Certificates\$TELEPORT_CA_CERT_SHA1" -ValueName "Blob" -Type Binary -Value $CERT + +$TeleportPEMFile = $env:TEMP + "\teleport.pem" +Write-Output $TELEPORT_CA_CERT_PEM | Out-File -FilePath $TeleportPEMFile + +$TeleportCRLFile = $env:TEMP + "\teleport-crl.pem" +Write-Output $TELEPORT_CRL_PEM | Out-File -FilePath $TeleportCRLFile + +certutil -dspublish -f $TeleportPEMFile RootCA +certutil -dspublish -f $TeleportPEMFile NTAuthCA +certutil -dspublish -f $TeleportCRLFile TeleportDB +certutil -pulse + +$ACCESS_SECURITY_TEMPLATE=@' +[Unicode] +Unicode=yes +[Version] +signature="$CHICAGO$" +[Service General Setting] +"SCardSvr",2,"" +'@ + +$COMMENT_XML=@' + + + + + + + + + + + + +'@ + +$ACCESS_POLICY_GUID=$ACCESS_GPO.Id.Guid.ToUpper() +$ACCESS_GPO_PATH="$env:SystemRoot\SYSVOL\sysvol\$DOMAIN_NAME\Policies\{$ACCESS_POLICY_GUID}\Machine\Microsoft\Windows NT\SecEdit" + +New-Item -Force -Type Directory -Path $ACCESS_GPO_PATH +New-Item -Force -Path $ACCESS_GPO_PATH -Name "GptTmpl.inf" -ItemType "file" -Value $ACCESS_SECURITY_TEMPLATE +New-Item -Force -Path "$env:SystemRoot\SYSVOL\sysvol\$DOMAIN_NAME\Policies\{$ACCESS_POLICY_GUID}\Machine" -Name "comment.cmtx" -ItemType "file" -Value $COMMENT_XML + +# # Step 2: Generate Teleport configuration. + +$WindowsDERFile = $env:TEMP + "\windows.der" +$WindowsPEMFile = $env:TEMP + "\windows.pem" +certutil "-ca.cert" $WindowsDERFile +certutil -encode $WindowsDERFile $WindowsPEMFile + +$CA_CERT_PEM = Get-Content -Path $WindowsPEMFile +$CA_CERT_YAML = $CA_CERT_PEM | ForEach-Object { " " + $_ } | Out-String + +$KDC_HOSTNAME = (Get-ADDomainController).HostName +# Get the SPN that contains the MSSQLSvc and the port number. +$SPN = (Get-ADComputer -Identity $COMPUTER_NAME -Properties servicePrincipalName).servicePrincipalName | Where-Object {$_ -like "MSSQLSvc/*:*"} + +$DATABASE_ACCESS_CONFIG_YAML=@" +version: v3 +teleport: + auth_token: $TELEPORT_PROVISION_TOKEN + proxy_server: $TELEPORT_PROXY_PUBLIC_ADDR + +auth_service: + enabled: no +ssh_service: + enabled: no +proxy_service: + enabled: no + +db_service: + enabled: "yes" + databases: + - name: $COMPUTER_NAME + protocol: sqlserver + uri: $DB_ADDRESS + ad: + domain: $DOMAIN_NAME + spn: $SPN + kdc_host_name: $KDC_HOSTNAME + ldap_cert: | +$CA_CERT_YAML +"@ + +$OUTPUT=@' + +Use the following teleport.yaml to start a Database Access Service. +For a detailed configuration reference, see + +https://goteleport.com/docs/database-access/reference/configuration/ + + +{0} + +'@ -f $DATABASE_ACCESS_CONFIG_YAML + +Write-Output $OUTPUT + +# Cleanup files that were created during execution of this script. +Remove-Item $TeleportPEMFile -Recurse +Remove-Item $TeleportCRLFile -Recurse +Remove-Item $WindowsDERFile -Recurse +Remove-Item $WindowsPEMFile -Recurse \ No newline at end of file