mirror of
https://github.com/gravitational/teleport.git
synced 2026-09-24 16:17:11 +08:00
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:
co-authored by
Isaiah Becker-Mayer
parent
4a3bd2c653
commit
0d885e231e
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user