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 <isaiah@goteleport.com>

* refactor(web): rename endpoint

---------

Co-authored-by: Isaiah Becker-Mayer <isaiah@goteleport.com>
This commit is contained in:
Gabriel Corado
2023-05-02 17:31:16 +00:00
committed by GitHub
co-authored by Isaiah Becker-Mayer
parent 4a3bd2c653
commit 0d885e231e
5 changed files with 326 additions and 0 deletions
+1
View File
@@ -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))
+82
View File
@@ -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,
})
}
+66
View File
@@ -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
}
+46
View File
@@ -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
}
@@ -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=@'
<?xml version='1.0' encoding='utf-8'?>
<policyComments xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.0" schemaVersion="1.0" xmlns="http://www.microsoft.com/GroupPolicy/CommentDefinitions">
<policyNamespaces>
<using prefix="ns0" namespace="Microsoft.Policies.TerminalServer"></using>
</policyNamespaces>
<comments>
<admTemplate></admTemplate>
</comments>
<resources minRequiredRevision="1.0">
<stringTable></stringTable>
</resources>
</policyComments>
'@
$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