Add Support for Oracle protocol (#23227)

This commit is contained in:
Marek Smoliński 2023-03-31 19:35:21 +02:00 committed by GitHub
parent 3fbe7f7ff4
commit 0f3c14e0f9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 759 additions and 52 deletions

View file

@ -511,6 +511,7 @@ func (d *DatabaseV3) CheckAndSetDefaults() error {
if err := d.Metadata.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
for key := range d.Spec.DynamicLabels {
if !IsValidLabelKey(key) {
return trace.BadParameter("database %q invalid label key: %q", d.GetName(), key)

View file

@ -62,6 +62,8 @@ const (
profileFileExt = ".yaml"
// fileLocalCA is the filename where a self-signed localhost CA cert is stored.
fileLocalCA = "localca.pem"
// oracleWalletDirSuffix is the suffix of the oracle wallet database directory.
oracleWalletDirSuffix = "-wallet"
)
// Here's the file layout of all these keypaths.
@ -90,9 +92,11 @@ const (
// │ ├── foo-db --> App access certs for user "foo"
// │ │ ├── root --> App access certs for cluster "root"
// │ │ │ ├── dbA-x509.pem --> TLS cert for database service "dbA"
// │ │ │ └── dbB-x509.pem --> TLS cert for database service "dbB"
// │ │ └── leaf --> App access certs for cluster "leaf"
// │ │ └── dbC-x509.pem --> TLS cert for database service "dbC"
// │ │ │ ├── dbB-x509.pem --> TLS cert for database service "dbB"
// │ │ │ └── dbC-wallet --> Oracle Client wallet Configuration directory.
// │ │ ├── leaf --> App access certs for cluster "leaf"
// │ │ │ └── dbC-x509.pem --> TLS cert for database service "dbC"
// │ │ └── proxy-localca.pem --> Self-signed TLS Routing local proxy CA
// │ ├── foo-kube --> Kubernetes certs for user "foo"
// │ | ├── root --> Kubernetes certs for Teleport cluster "root"
// │ | │ ├── kubeA-kubeconfig --> standalone kubeconfig for Kubernetes cluster "kubeA"
@ -278,6 +282,13 @@ func DatabaseCertPath(baseDir, proxy, username, cluster, dbname string) string {
return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+fileExtTLSCert)
}
// DatabaseOracleWalletDirectory returns the path to the user's Oracle Wallet configuration directory.
// for the given proxy, cluster and database.
// <baseDir>/keys/<proxy>/<username>-db/<cluster>/dbname-wallet/
func DatabaseOracleWalletDirectory(baseDir, proxy, username, cluster, dbname string) string {
return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+oracleWalletDirSuffix)
}
// KubeDir returns the path to the user's kube directory
// for the given proxy.
//

1
go.mod
View file

@ -162,6 +162,7 @@ require (
sigs.k8s.io/controller-runtime v0.14.6
sigs.k8s.io/controller-tools v0.11.3
sigs.k8s.io/yaml v1.3.0
software.sslmate.com/src/go-pkcs12 v0.2.0
)
// Indirect mailgun dependencies.

2
go.sum
View file

@ -1830,4 +1830,6 @@ sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ih
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE=
software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ=
sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU=

View file

@ -41,8 +41,8 @@ type GenerateDatabaseCertificatesRequest struct {
IdentityFileWriter identityfile.ConfigWriter
TTL time.Duration
Key *client.Key
// JKSKeyStore is used to generate JKS keystore used for cassandra format.
JKSPassword string
// Password is used to generate JKS keystore used for cassandra format or Oracle wallet.
Password string
}
// GenerateDatabaseCertificates to be used by databases to set up mTLS authentication
@ -123,7 +123,7 @@ func GenerateDatabaseCertificates(ctx context.Context, req GenerateDatabaseCerti
Format: req.OutputFormat,
OverwriteDestination: req.OutputCanOverwrite,
Writer: req.IdentityFileWriter,
JKSPassword: req.JKSPassword,
Password: req.Password,
})
if err != nil {
return nil, trace.Wrap(err)

View file

@ -69,6 +69,8 @@ const (
elasticsearchSQLBin = "elasticsearch-sql-cli"
// awsBin is the aws CLI program name.
awsBin = "aws"
// oracleBin is the Oracle CLI program name.
oracleBin = "sql"
)
// Execer is an abstraction of Go's exec module, as this one doesn't specify any interfaces.
@ -192,6 +194,9 @@ func (c *CLICommandBuilder) GetConnectCommand() (*exec.Cmd, error) {
case defaults.ProtocolDynamoDB:
return c.getDynamoDBCommand()
case defaults.ProtocolOracle:
return c.getOracleCommand()
}
return nil, trace.BadParameter("unsupported database protocol: %v", c.db)
@ -619,6 +624,36 @@ func (c *CLICommandBuilder) getDynamoDBCommand() (*exec.Cmd, error) {
return c.options.exe.Command(awsBin, args...), nil
}
type jdbcOracleThinConnection struct {
host string
port int
db string
tnsAdmin string
}
func (j *jdbcOracleThinConnection) ConnString() string {
return fmt.Sprintf(`jdbc:oracle:thin:@tcps://%s:%d/%s?TNS_ADMIN=%s`, j.host, j.port, j.db, j.tnsAdmin)
}
func (c *CLICommandBuilder) getOracleCommand() (*exec.Cmd, error) {
cs := jdbcOracleThinConnection{
host: c.host,
port: c.port,
db: c.db.Database,
tnsAdmin: c.profile.OracleWalletDir(c.profile.Cluster, c.db.ServiceName),
}
// Quote the address for printing as the address contains "?".
connString := cs.ConnString()
if c.options.printFormat {
connString = fmt.Sprintf(`'%s'`, connString)
}
args := []string{
"-L", // dont retry
connString,
}
return c.options.exe.Command(oracleBin, args...), nil
}
func (c *CLICommandBuilder) getElasticsearchAlternativeCommands() []CommandAlternative {
var commands []CommandAlternative
if c.isElasticsearchSQLBinAvailable() {

View file

@ -585,6 +585,24 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) {
cmd: []string{"aws", "--endpoint", "http://localhost:12345/", "[dynamodb|dynamodbstreams|dax]", "<command>"},
wantErr: false,
},
{
name: "oracle",
dbProtocol: defaults.ProtocolOracle,
opts: []ConnectCommandFunc{WithLocalProxy("localhost", 12345, "")},
execer: &fakeExec{},
databaseName: "oracle01",
cmd: []string{"sql", "-L", "jdbc:oracle:thin:@tcps://localhost:12345/oracle01?TNS_ADMIN=/tmp/keys/example.com/bob-db/mysql-wallet"},
wantErr: false,
},
{
name: "Oracle with print format",
dbProtocol: defaults.ProtocolOracle,
opts: []ConnectCommandFunc{WithLocalProxy("localhost", 12345, ""), WithPrintFormat()},
execer: &fakeExec{},
databaseName: "oracle01",
cmd: []string{"sql", "-L", "'jdbc:oracle:thin:@tcps://localhost:12345/oracle01?TNS_ADMIN=/tmp/keys/example.com/bob-db/mysql-wallet'"},
wantErr: false,
},
}
for _, tt := range tests {

View file

@ -0,0 +1,115 @@
/*
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 oracle
import (
"bytes"
"os"
"path/filepath"
"text/template"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
)
type jdbcSettings struct {
KeyStoreFile string
TrustStoreFile string
KeyStorePassword string
TrustStorePassword string
}
const jdbcPropertiesTemplateContent = `
javax.net.ssl.keyStore={{.KeyStoreFile}}
javax.net.ssl.trustStore={{.TrustStoreFile}}
javax.net.ssl.keyStorePassword={{.KeyStorePassword}}
javax.net.ssl.trustStorePassword={{.TrustStorePassword}}
javax.net.ssl.keyStoreType=jks
javax.net.ssl.trustStoreType=jks
oracle.net.authentication_services=TCPS
`
type tnsNamesORASettings struct {
ServiceName string
Host string
Port string
}
const sqlnetORATemplateContent = `
SSL_CLIENT_AUTHENTICATION = TRUE
SQLNET.AUTHENTICATION_SERVICES = (TCPS)
WALLET_LOCATION =
(SOURCE =
(METHOD = FILE)
(METHOD_DATA =
(DIRECTORY = {{.WalletDir}})
)
)
`
type sqlnetORASettings struct {
WalletDir string
}
const tnsnamesORATemplateContent = `
{{.ServiceName}} =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCPS)(HOST = {{.Host}})(PORT = {{.Port}}))
)
(CONNECT_DATA =
(SERVER = DEDICATED)
(SERVICE_NAME = {{.ServiceName}})
)
(SECURITY =
(SSL_SERVER_CERT_DN = "CN=localhost")
)
)
`
var (
jdbcPropertiesTemplate = template.Must(template.New("").Parse(jdbcPropertiesTemplateContent))
sqlnetORATemplate = template.Must(template.New("").Parse(sqlnetORATemplateContent))
tnsnamesORATemplate = template.Must(template.New("").Parse(tnsnamesORATemplateContent))
)
func (c jdbcSettings) template() *template.Template { return jdbcPropertiesTemplate }
func (c sqlnetORASettings) template() *template.Template { return sqlnetORATemplate }
func (c tnsNamesORASettings) template() *template.Template { return tnsnamesORATemplate }
func (c jdbcSettings) configFilename() string { return "ojdbc.properties" }
func (c sqlnetORASettings) configFilename() string { return "sqlnet.ora" }
func (c tnsNamesORASettings) configFilename() string { return "tnsnames.ora" }
type templateSettings interface {
template() *template.Template
configFilename() string
}
func writeSettings(settings templateSettings, dir string) error {
var buff bytes.Buffer
if err := settings.template().Execute(&buff, settings); err != nil {
return trace.Wrap(err)
}
filePath := filepath.Join(dir, settings.configFilename())
if err := os.WriteFile(filePath, buff.Bytes(), teleport.FileMaskOwnerOnly); err != nil {
return trace.Wrap(err)
}
return nil
}

View file

@ -0,0 +1,144 @@
/*
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 oracle
import (
"bytes"
"crypto/x509"
"os"
"path/filepath"
"time"
"github.com/gravitational/trace"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
// GenerateClientConfiguration function generates following Oracle Client configuration:
// wallet.jks - Java Wallet format used by JDBC Drivers.
// sqlnet.ora - Generic Oracle Client Configuration File allowing to specify Wallet Location.
// tnsnames.ora - Oracle Net Service mapped to connections descriptors.
func GenerateClientConfiguration(key *client.Key, db tlsca.RouteToDatabase, profile *client.ProfileStatus) error {
walletPath := profile.OracleWalletDir(key.ClusterName, db.ServiceName)
if err := os.MkdirAll(walletPath, teleport.PrivateDirMode); err != nil {
return trace.Wrap(err)
}
password, err := utils.CryptoRandomHex(32)
if err != nil {
return trace.Wrap(err)
}
localProxyCAPem, err := os.ReadFile(profile.DatabaseLocalCAPath())
if err != nil {
return trace.ConvertSystemError(err)
}
jksWalletPath, err := createClientWallet(key, localProxyCAPem, password, walletPath)
if err != nil {
return trace.Wrap(err)
}
err = writeClientConfig(walletPath, jksWalletPath, password)
if err != nil {
return trace.Wrap(err)
}
return nil
}
func createClientWallet(key *client.Key, certPem []byte, password string, walletPath string) (string, error) {
buff, err := createJKSWallet(key.PrivateKeyPEM(), certPem, certPem, password)
if err != nil {
return "", trace.Wrap(err)
}
walletFile := filepath.Join(walletPath, "wallet.jks")
if err := os.WriteFile(walletFile, buff, teleport.FileMaskOwnerOnly); err != nil {
return "", trace.Wrap(err)
}
return walletFile, nil
}
func createJKSWallet(keyPEM, certPEM, caPEM []byte, password string) ([]byte, error) {
key, err := utils.ParsePrivateKey(keyPEM)
if err != nil {
return nil, trace.Wrap(err)
}
privateKey, err := x509.MarshalPKCS8PrivateKey(key)
if err != nil {
return nil, trace.Wrap(err)
}
ks := keystore.New()
pkeIn := keystore.PrivateKeyEntry{
CreationTime: time.Now(),
PrivateKey: privateKey,
CertificateChain: []keystore.Certificate{
{
Type: "x509",
Content: certPEM,
},
},
}
if err := ks.SetPrivateKeyEntry("teleportUserCert", pkeIn, []byte(password)); err != nil {
return nil, trace.Wrap(err)
}
trustIn := keystore.TrustedCertificateEntry{
CreationTime: time.Now(),
Certificate: keystore.Certificate{
Type: "x509",
Content: caPEM,
},
}
if err := ks.SetTrustedCertificateEntry("teleportLocalCA", trustIn); err != nil {
return nil, trace.Wrap(err)
}
var buff bytes.Buffer
if err := ks.Store(&buff, []byte(password)); err != nil {
return nil, trace.Wrap(err)
}
return buff.Bytes(), nil
}
func writeClientConfig(path string, jksFile string, password string) error {
var clientConfiguration = []templateSettings{
tnsNamesORASettings{
Host: "localhost",
// User default values that will be overwritten by JDBC connection string.
ServiceName: "XE",
Port: "2484",
},
sqlnetORASettings{
WalletDir: path,
},
jdbcSettings{
KeyStoreFile: jksFile,
TrustStoreFile: jksFile,
KeyStorePassword: password,
TrustStorePassword: password,
},
}
for _, v := range clientConfiguration {
if err := writeSettings(v, path); err != nil {
return trace.Wrap(err, "Failed to write %v", v.configFilename())
}
}
return nil
}

View file

@ -20,17 +20,20 @@ package identityfile
import (
"bytes"
"context"
"crypto/rand"
"crypto/x509"
"encoding/pem"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gravitational/trace"
"github.com/pavlo-v-chernykh/keystore-go/v4"
"software.sslmate.com/src/go-pkcs12"
"github.com/gravitational/teleport/api/identityfile"
"github.com/gravitational/teleport/api/profile"
@ -99,6 +102,12 @@ const (
// DefaultFormat is what Teleport uses by default
DefaultFormat = FormatFile
// FormatOracle produces CA and ke pair in the Oracle wallet format.
// The execution depend on Orapki binary and if this binary is not found
// Teleport will print intermediate steps how to convert Teleport certs
// to Oracle wallet on Oracle Server instance.
FormatOracle Format = "oracle"
)
// FormatList is a list of all possible FormatList.
@ -108,6 +117,7 @@ type FormatList []Format
var KnownFileFormats = FormatList{
FormatFile, FormatOpenSSH, FormatTLS, FormatKubernetes, FormatDatabase, FormatWindows,
FormatMongo, FormatCockroach, FormatRedis, FormatSnowflake, FormatElasticsearch, FormatCassandra, FormatScylla,
FormatOracle,
}
// String returns human-readable version of FormatList, ex:
@ -181,8 +191,8 @@ type WriteConfig struct {
OverwriteDestination bool
// Writer is the filesystem implementation.
Writer ConfigWriter
// JKSPassword is the password for the JKS keystore used by Cassandra format.
JKSPassword string
// Password is the password for the JKS keystore used by Cassandra format and Oracle wallet.
Password string
}
// Write writes user credentials to disk in a specified format.
@ -309,7 +319,6 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err
if err != nil {
return nil, trace.Wrap(err)
}
// FormatMongo is the same as FormatTLS or FormatDatabase certificate and
// key are concatenated in the same .crt file which is what Mongo expects.
case FormatMongo:
@ -371,6 +380,12 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err
return nil, trace.Wrap(err)
}
filesWritten = append(filesWritten, out...)
case FormatOracle:
out, err := writeOracleFormat(cfg, writer)
if err != nil {
return nil, trace.Wrap(err)
}
filesWritten = append(filesWritten, out...)
case FormatKubernetes:
filesWritten = append(filesWritten, cfg.OutputPath)
@ -410,12 +425,12 @@ func Write(ctx context.Context, cfg WriteConfig) (filesWritten []string, err err
}
func writeCassandraFormat(cfg WriteConfig, writer ConfigWriter) ([]string, error) {
if cfg.JKSPassword == "" {
if cfg.Password == "" {
pass, err := utils.CryptoRandomHex(16)
if err != nil {
return nil, trace.Wrap(err)
}
cfg.JKSPassword = pass
cfg.Password = pass
}
// Cassandra expects a JKS keystore file with the private key and certificate
// in it. The keystore file is password protected.
@ -445,6 +460,116 @@ func writeCassandraFormat(cfg WriteConfig, writer ConfigWriter) ([]string, error
return []string{certPath, casPath}, nil
}
// writeOracleFormat creates an Oracle wallet files if orapki Oracle tool is available
// is user env otherwise creates a p12 key-pair file allowing to run orapki on the Oracle server
// and create the Oracle wallet manually.
func writeOracleFormat(cfg WriteConfig, writer ConfigWriter) ([]string, error) {
certBlock, err := tlsca.ParseCertificatePEM(cfg.Key.TLSCert)
if err != nil {
return nil, trace.Wrap(err)
}
keyK, err := utils.ParsePrivateKeyPEM(cfg.Key.PrivateKeyPEM())
if err != nil {
return nil, trace.Wrap(err)
}
var caCerts []*x509.Certificate
for _, ca := range cfg.Key.TrustedCerts {
for _, cert := range ca.TLSCertificates {
c, err := tlsca.ParseCertificatePEM(cert)
if err != nil {
return nil, trace.Wrap(err)
}
caCerts = append(caCerts, c)
}
}
pf, err := pkcs12.Encode(rand.Reader, keyK, certBlock, caCerts, cfg.Password)
if err != nil {
return nil, trace.Wrap(err)
}
p12Path := cfg.OutputPath + ".p12"
certPath := cfg.OutputPath + ".crt"
if err := writer.WriteFile(p12Path, pf, identityfile.FilePermissions); err != nil {
return nil, trace.Wrap(err)
}
err = writer.WriteFile(certPath, cfg.Key.TLSCert, identityfile.FilePermissions)
if err != nil {
return nil, trace.Wrap(err)
}
// Is ORAPKI binary is available is user env run command ang generate autologin Oracle wallet.
if isOrapkiAvailable() {
// Is Orapki is available in the user env create the Oracle wallet directly.
// otherwise Orapki tool needs to be executed on the server site to import keypair to
// Oracle wallet.
if err := createOracleWallet(cfg.OutputPath, p12Path, certPath, cfg.Password); err != nil {
return nil, trace.Wrap(err)
}
// If Oracle Wallet was created the raw p12 keypair and trusted cert are no longer needed.
if err := os.Remove(p12Path); err != nil {
return nil, trace.Wrap(err)
}
if err := os.Remove(certPath); err != nil {
return nil, trace.Wrap(err)
}
// Return the path to the Oracle wallet.
return []string{cfg.OutputPath}, nil
}
// Otherwise return destinations to p12 keypair and trusted CA allowing a user to run the convert flow on the
// Oracle server instance in order to create Oracle wallet file.
return []string{p12Path, certPath}, nil
}
const (
orapkiBinary = "orapki"
)
func isOrapkiAvailable() bool {
_, err := exec.LookPath(orapkiBinary)
return err == nil
}
func createOracleWallet(walletPath, p12Path, certPath, password string) error {
errDetailsFormat := "\n\nOrapki command:\n%s \n\nCompleted with following error: \n%s"
// Create Raw Oracle wallet with auto_login_only flag - no password required.
args := []string{
"wallet", "create", "-wallet", walletPath,
"-auto_login_only",
}
cmd := exec.Command(orapkiBinary, args...)
if output, err := cmd.CombinedOutput(); err != nil {
return trace.Wrap(err, fmt.Sprintf(errDetailsFormat, cmd.String(), output))
}
// Import keypair into oracle wallet as a user cert.
args = []string{
"wallet", "import_pkcs12", "-wallet", walletPath,
"-auto_login_only",
"-pkcs12file", p12Path,
"-pkcs12pwd", password,
}
cmd = exec.Command(orapkiBinary, args...)
if output, err := exec.Command(orapkiBinary, args...).CombinedOutput(); err != nil {
return trace.Wrap(err, fmt.Sprintf(errDetailsFormat, cmd.String(), output))
}
// Add import teleport CA to the oracle wallet.
args = []string{
"wallet", "add", "-wallet", walletPath,
"-trusted_cert",
"-auto_login_only",
"-cert", certPath,
}
cmd = exec.Command(orapkiBinary, args...)
if output, err := exec.Command(orapkiBinary, args...).CombinedOutput(); err != nil {
return trace.Wrap(err, fmt.Sprintf(errDetailsFormat, cmd.String(), output))
}
return nil
}
func prepareCassandraTruststore(cfg WriteConfig) (*bytes.Buffer, error) {
var caCerts []byte
for _, ca := range cfg.Key.TrustedCerts {
@ -466,7 +591,7 @@ func prepareCassandraTruststore(cfg WriteConfig) (*bytes.Buffer, error) {
return nil, trace.Wrap(err)
}
var buff bytes.Buffer
if err := ks.Store(&buff, []byte(cfg.JKSPassword)); err != nil {
if err := ks.Store(&buff, []byte(cfg.Password)); err != nil {
return nil, trace.Wrap(err)
}
return &buff, nil
@ -497,11 +622,11 @@ func prepareCassandraKeystore(cfg WriteConfig) (*bytes.Buffer, error) {
},
},
}
if err := ks.SetPrivateKeyEntry("cassandra", pkeIn, []byte(cfg.JKSPassword)); err != nil {
if err := ks.SetPrivateKeyEntry("cassandra", pkeIn, []byte(cfg.Password)); err != nil {
return nil, trace.Wrap(err)
}
var buff bytes.Buffer
if err := ks.Store(&buff, []byte(cfg.JKSPassword)); err != nil {
if err := ks.Store(&buff, []byte(cfg.Password)); err != nil {
return nil, trace.Wrap(err)
}
return &buff, nil

View file

@ -367,7 +367,7 @@ func isTeleportAgentKey(key *agent.Key) bool {
return strings.HasPrefix(key.Comment, agentKeyCommentPrefix+agentKeyCommentSeparator)
}
// AsAgentKeys converts client.Key struct to an agent.AddedKey. Any agent.AddedKey
// AsAgentKey converts client.Key struct to an agent.AddedKey. Any agent.AddedKey
// can be added to a local agent (keyring), nut non-standard keys cannot be added
// to an SSH system agent through the ssh agent protocol. Check canAddToSystemAgent
// before adding this key to an SSH system agent.

View file

@ -449,6 +449,36 @@ func (p *ProfileStatus) DatabaseCertPathForCluster(clusterName string, databaseN
return keypaths.DatabaseCertPath(p.Dir, p.Name, p.Username, clusterName, databaseName)
}
// OracleWalletDir returns path to the specified database access
// certificate for this profile, for the specified cluster.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-db/<cluster>/dbname-wallet/
//
// If the input cluster name is an empty string, the selected cluster in the
// profile will be used.
func (p *ProfileStatus) OracleWalletDir(clusterName string, databaseName string) string {
if clusterName == "" {
clusterName = p.Cluster
}
if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, VirtualPathDatabaseParams(databaseName)); ok {
return path
}
return keypaths.DatabaseOracleWalletDirectory(p.Dir, p.Name, p.Username, clusterName, databaseName)
}
// DatabaseLocalCAPath returns the specified db 's self-signed localhost CA path for
// this profile.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-db/proxy-localca.pem
func (p *ProfileStatus) DatabaseLocalCAPath() string {
if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, nil); ok {
return path
}
return filepath.Join(keypaths.DatabaseDir(p.Dir, p.Name, p.Username), "proxy-localca.pem")
}
// AppCertPath returns path to the specified app access certificate
// for this profile.
//

View file

@ -421,6 +421,8 @@ const (
ProtocolMySQL = "mysql"
// ProtocolMongoDB is the MongoDB database protocol.
ProtocolMongoDB = "mongodb"
// ProtocolOracle is the Oracle database protocol.
ProtocolOracle = "oracle"
// ProtocolRedis is the Redis database protocol.
ProtocolRedis = "redis"
// ProtocolCockroachDB is the CockroachDB database protocol.
@ -446,6 +448,7 @@ var DatabaseProtocols = []string{
ProtocolPostgres,
ProtocolMySQL,
ProtocolMongoDB,
ProtocolOracle,
ProtocolCockroachDB,
ProtocolRedis,
ProtocolSnowflake,
@ -465,6 +468,8 @@ func ReadableDatabaseProtocol(p string) string {
return "MySQL"
case ProtocolMongoDB:
return "MongoDB"
case ProtocolOracle:
return "Oracle"
case ProtocolCockroachDB:
return "CockroachDB"
case ProtocolRedis:

View file

@ -3159,6 +3159,7 @@ func (process *TeleportProcess) setupProxyListeners(networkingConfig types.Clust
}
listeners.db.postgres = listener
}
}
tunnelStrategy, err := networkingConfig.GetTunnelStrategyType()
@ -4041,6 +4042,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
// route extracted connection to ALPN Proxy DB TLS Handler.
MatchFunc: alpnproxy.MatchByProtocol(
alpncommon.ProtocolMongoDB,
alpncommon.ProtocolOracle,
alpncommon.ProtocolRedisDB,
alpncommon.ProtocolSnowflake,
alpncommon.ProtocolSQLServer,

View file

@ -489,6 +489,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-postgres-ping",
"teleport-mysql-ping",
"teleport-mongodb-ping",
"teleport-oracle-ping",
"teleport-redis-ping",
"teleport-sqlserver-ping",
"teleport-snowflake-ping",
@ -505,6 +506,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-postgres",
"teleport-mysql",
"teleport-mongodb",
"teleport-oracle",
"teleport-redis",
"teleport-sqlserver",
"teleport-snowflake",
@ -521,6 +523,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-postgres-ping",
"teleport-mysql-ping",
"teleport-mongodb-ping",
"teleport-oracle-ping",
"teleport-redis-ping",
"teleport-sqlserver-ping",
"teleport-snowflake-ping",
@ -540,6 +543,7 @@ func TestSetupProxyTLSConfig(t *testing.T) {
"teleport-postgres",
"teleport-mysql",
"teleport-mongodb",
"teleport-oracle",
"teleport-redis",
"teleport-sqlserver",
"teleport-snowflake",

View file

@ -24,6 +24,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db/common/enterprise"
awsutils "github.com/gravitational/teleport/lib/utils/aws"
)
@ -73,6 +74,9 @@ type Database struct {
// CheckAndSetDefaults validates the database proxy configuration.
func (d *Database) CheckAndSetDefaults() error {
if err := enterprise.ProtocolValidation(d.Protocol); err != nil {
return trace.Wrap(err)
}
if d.Name == "" {
return trace.BadParameter("empty database name")
}

View file

@ -50,6 +50,7 @@ import (
libcloudaws "github.com/gravitational/teleport/lib/cloud/aws"
"github.com/gravitational/teleport/lib/cloud/azure"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/srv/db/common/enterprise"
"github.com/gravitational/teleport/lib/srv/db/redis/connection"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
@ -137,6 +138,9 @@ func UnmarshalDatabase(data []byte, opts ...MarshalOption) (types.Database, erro
// ValidateDatabase validates a types.Database.
func ValidateDatabase(db types.Database) error {
if err := enterprise.ProtocolValidation(db.GetProtocol()); err != nil {
return trace.Wrap(err)
}
if err := db.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}

View file

@ -39,6 +39,9 @@ const (
// ProtocolMongoDB is TLS ALPN protocol value used to indicate Mongo protocol.
ProtocolMongoDB Protocol = "teleport-mongodb"
// ProtocolOracle is TLS ALPN protocol value used to indicate Oracle protocol.
ProtocolOracle Protocol = "teleport-oracle"
// ProtocolRedisDB is TLS ALPN protocol value used to indicate Redis protocol.
ProtocolRedisDB Protocol = "teleport-redis"
@ -145,6 +148,8 @@ func ToALPNProtocol(dbProtocol string) (Protocol, error) {
return ProtocolPostgres, nil
case defaults.ProtocolMongoDB:
return ProtocolMongoDB, nil
case defaults.ProtocolOracle:
return ProtocolOracle, nil
case defaults.ProtocolRedis:
return ProtocolRedisDB, nil
case defaults.ProtocolSQLServer:
@ -170,6 +175,7 @@ func ToALPNProtocol(dbProtocol string) (Protocol, error) {
func IsDBTLSProtocol(protocol Protocol) bool {
dbTLSProtocols := []Protocol{
ProtocolMongoDB,
ProtocolOracle,
ProtocolRedisDB,
ProtocolSQLServer,
ProtocolSnowflake,
@ -188,6 +194,7 @@ var DatabaseProtocols = []Protocol{
ProtocolPostgres,
ProtocolMySQL,
ProtocolMongoDB,
ProtocolOracle,
ProtocolRedisDB,
ProtocolSQLServer,
ProtocolSnowflake,

View file

@ -26,6 +26,7 @@ import (
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/cloud"
"github.com/gravitational/teleport/lib/srv/db/common/enterprise"
)
var (
@ -69,6 +70,10 @@ func CheckEngines(names ...string) error {
enginesMu.RLock()
defer enginesMu.RUnlock()
for _, name := range names {
if err := enterprise.ProtocolValidation(name); err != nil {
// Don't assert Enterprise protocol is a build is OSS
continue
}
if engines[name] == nil {
return trace.NotFound("database engine %q is not registered", name)
}

View file

@ -0,0 +1,35 @@
/*
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 enterprise
import (
"github.com/gravitational/trace"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/modules"
)
// ProtocolValidation checks if protocol is supported for current build.
func ProtocolValidation(dbProtocol string) error {
switch dbProtocol {
case defaults.ProtocolOracle:
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return trace.BadParameter("%s database protocol is only available with an enterprise license", dbProtocol)
}
}
return nil
}

View file

@ -44,6 +44,7 @@ import (
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/common/enterprise"
"github.com/gravitational/teleport/lib/srv/db/dbutils"
"github.com/gravitational/teleport/lib/srv/db/mysql"
"github.com/gravitational/teleport/lib/srv/db/postgres"
@ -333,6 +334,9 @@ func (s *ProxyServer) handleConnection(conn net.Conn) error {
s.cfg.IngressReporter.ConnectionAuthenticated(ingress.DatabaseTLS, conn)
defer s.cfg.IngressReporter.AuthenticatedConnectionClosed(ingress.DatabaseTLS, conn)
}
if enterprise.ProtocolValidation(proxyCtx.Identity.RouteToDatabase.Protocol); err != nil {
return trace.Wrap(err)
}
switch proxyCtx.Identity.RouteToDatabase.Protocol {
case defaults.ProtocolPostgres, defaults.ProtocolCockroachDB:
@ -345,6 +349,7 @@ func (s *ProxyServer) handleConnection(conn net.Conn) error {
case defaults.ProtocolSQLServer:
return s.SQLServerProxy().HandleConnection(s.closeCtx, proxyCtx, tlsConn)
}
serviceConn, err := s.Connect(s.closeCtx, proxyCtx, conn.RemoteAddr(), conn.LocalAddr())
if err != nil {
return trace.Wrap(err)

View file

@ -236,3 +236,13 @@ func overwriteFile(filePath string) (err error) {
_, err = io.CopyN(f, rand.Reader, size)
return trace.Wrap(err)
}
// RemoveFileIfExist removes file if exits.
func RemoveFileIfExist(filePath string) {
if !FileExists(filePath) {
return
}
if err := os.Remove(filePath); err != nil {
log.WithError(err).Warnf("Failed to remove %v", filePath)
}
}

View file

@ -21,6 +21,7 @@ import (
"net"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"
@ -74,7 +75,7 @@ type AuthCommand struct {
windowsDomain string
windowsSID string
signOverwrite bool
jksPassword string
password string
caType string
rotateGracePeriod time.Duration
@ -256,12 +257,20 @@ func (a *AuthCommand) GenerateAndSignKeys(ctx context.Context, clusterAPI auth.C
if err != nil {
return trace.Wrap(err)
}
a.jksPassword = jskPass
a.password = jskPass
return a.generateDatabaseKeys(ctx, clusterAPI)
case identityfile.FormatSnowflake:
return a.generateSnowflakeKey(ctx, clusterAPI)
case identityfile.FormatWindows:
return a.generateWindowsCert(ctx, clusterAPI)
case identityfile.FormatOracle:
oracleWalletPass, err := utils.CryptoRandomHex(32)
if err != nil {
return trace.Wrap(err)
}
a.password = oracleWalletPass
return a.generateDBOracleCert(ctx, clusterAPI)
}
switch {
case a.genUser != "" && a.genHost == "":
@ -498,14 +507,14 @@ func (a *AuthCommand) generateDatabaseKeysForKey(ctx context.Context, clusterAPI
OutputLocation: a.output,
TTL: a.genTTL,
Key: key,
JKSPassword: a.jksPassword,
Password: a.password,
}
filesWritten, err := db.GenerateDatabaseCertificates(ctx, dbCertReq)
if err != nil {
return trace.Wrap(err)
}
return trace.Wrap(writeHelperMessageDBmTLS(os.Stdout, filesWritten, a.output, a.outputFormat, a.jksPassword))
return trace.Wrap(writeHelperMessageDBmTLS(os.Stdout, filesWritten, a.output, a.outputFormat, a.password))
}
var mapIdentityFileFormatHelperTemplate = map[identityfile.Format]*template.Template{
@ -517,9 +526,10 @@ var mapIdentityFileFormatHelperTemplate = map[identityfile.Format]*template.Temp
identityfile.FormatElasticsearch: elasticsearchAuthSignTpl,
identityfile.FormatCassandra: cassandraAuthSignTpl,
identityfile.FormatScylla: scyllaAuthSignTpl,
identityfile.FormatOracle: oracleAuthSignTpl,
}
func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output string, outputFormat identityfile.Format, jksPassword string) error {
func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output string, outputFormat identityfile.Format, password string) error {
if writer == nil {
return nil
}
@ -531,9 +541,13 @@ func writeHelperMessageDBmTLS(writer io.Writer, filesWritten []string, output st
return nil
}
tplVars := map[string]interface{}{
"files": strings.Join(filesWritten, ", "),
"jksPassword": jksPassword,
"output": output,
"files": strings.Join(filesWritten, ", "),
"password": password,
"output": output,
}
if outputFormat == defaults.ProtocolOracle {
tplVars["manualOrapkiFlow"] = len(filesWritten) != 1
tplVars["walletDir"] = filepath.Dir(output)
}
return trace.Wrap(tpl.Execute(writer, tplVars))
@ -618,25 +632,50 @@ https://www.elastic.co/guide/en/elasticsearch/reference/current/security-setting
`))
cassandraAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Cassandra server, add the following to your
cassandra.yaml configuration file:
client_encryption_options:
enabled: true
optional: false
keystore: /path/to/{{.output}}.keystore
keystore_password: "{{.jksPassword}}"
keystore_password: "{{.password}}"
require_client_auth: true
truststore: /path/to/{{.output}}.truststore
truststore_password: "{{.jksPassword}}"
truststore_password: "{{.password}}"
protocol: TLS
algorithm: SunX509
store_type: JKS
cipher_suites: [TLS_RSA_WITH_AES_256_CBC_SHA]
`))
oracleAuthSignTpl = template.Must(template.New("").Parse(`
{{if .manualOrapkiFlow}}
Orapki binary was not found. Please create oracle wallet file manually by running the following commands on the Oracle server:
orapki wallet create -wallet {{.walletDir}} -auto_login_only
orapki wallet import_pkcs12 -wallet {{.walletDir}} -auto_login_only -pkcs12file {{.output}}.p12 -pkcs12pwd {{.password}}
orapki wallet add -wallet {{.walletDir}} -trusted_cert -auto_login_only -cert {{.output}}.crt
{{end}}
To enable mutual TLS on your Oracle server, add the following settings to Oracle sqlnet.ora configuration file:
WALLET_LOCATION = (SOURCE = (METHOD = FILE)(METHOD_DATA = (DIRECTORY = /path/to/oracleWalletDir)))
SSL_CLIENT_AUTHENTICATION = TRUE
SQLNET.AUTHENTICATION_SERVICES = (TCPS)
To enable mutual TLS on your Oracle server, add the following TCPS entries to listener.ora configuration file:
LISTENER =
(DESCRIPTION_LIST =
(DESCRIPTION =
(ADDRESS = (PROTOCOL = TCPS)(HOST = 0.0.0.0)(PORT = 2484))
)
)
WALLET_LOCATION = (SOURCE = (METHOD = FILE)(METHOD_DATA = (DIRECTORY = /path/to/oracleWalletDir)))
SSL_CLIENT_AUTHENTICATION = TRUE
`))
scyllaAuthSignTpl = template.Must(template.New("").Parse(`Database credentials have been written to {{.files}}.
To enable mutual TLS on your Scylla server, add the following to your
@ -943,6 +982,14 @@ func (a *AuthCommand) checkProxyAddr(ctx context.Context, clusterAPI auth.Client
return trace.BadParameter("couldn't find registered public proxies, specify --proxy when using --format=%q", identityfile.FormatKubernetes)
}
func (a *AuthCommand) generateDBOracleCert(ctx context.Context, api auth.ClientI) error {
key, err := client.GenerateRSAKey()
if err != nil {
return trace.Wrap(err)
}
return a.generateDatabaseKeysForKey(ctx, api, key)
}
func parseURL(rawurl string) (*url.URL, error) {
u, err := url.Parse(rawurl)
if err != nil {

View file

@ -487,18 +487,7 @@ func pickActiveApp(cf *CLIConf) (*tlsca.RouteToApp, error) {
// removeAppLocalFiles removes generated local files for the provided app.
func removeAppLocalFiles(profile *client.ProfileStatus, appName string) {
removeFileIfExist(profile.AppLocalCAPath(appName))
}
// removeFileIfExist removes a local file if it exists.
func removeFileIfExist(filePath string) {
if !utils.FileExists(filePath) {
return
}
if err := os.Remove(filePath); err != nil {
log.WithError(err).Warnf("Failed to remove %v", filePath)
}
utils.RemoveFileIfExist(profile.AppLocalCAPath(appName))
}
// loadAppSelfSignedCA loads self-signed CA for provided app, or tries to

View file

@ -42,6 +42,7 @@ import (
"github.com/gravitational/teleport/lib/client"
dbprofile "github.com/gravitational/teleport/lib/client/db"
"github.com/gravitational/teleport/lib/client/db/dbcmd"
"github.com/gravitational/teleport/lib/client/db/oracle"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
@ -294,7 +295,7 @@ func checkAndSetDBRouteDefaults(r *tlsca.RouteToDatabase) error {
// Elasticsearch needs database username too.
if r.Username == "" {
switch r.Protocol {
case defaults.ProtocolMongoDB, defaults.ProtocolElasticsearch:
case defaults.ProtocolMongoDB, defaults.ProtocolElasticsearch, defaults.ProtocolOracle:
return trace.BadParameter("please provide the database user name using the --db-user flag")
case defaults.ProtocolRedis:
// Default to "default" in the same way as Redis does. We need the username to check access on our side.
@ -309,6 +310,12 @@ func checkAndSetDBRouteDefaults(r *tlsca.RouteToDatabase) error {
r.ServiceName, defaults.ReadableDatabaseProtocol(r.Protocol), r.Database)
r.Database = ""
}
} else {
switch r.Protocol {
// Always require db-name for Oracle Protocol.
case defaults.ProtocolOracle:
return trace.BadParameter("please provide the database name using the --db-name flag")
}
}
return nil
}
@ -324,12 +331,12 @@ func databaseLogin(cf *CLIConf, tc *client.TeleportClient, route tlsca.RouteToDa
return trace.Wrap(err)
}
var key *client.Key
// Identity files themselves act as the database credentials (if any), so
// don't bother fetching new certs.
if profile.IsVirtual {
log.Info("Note: already logged in due to an identity file (`-i ...`); will only update database config files.")
} else {
var key *client.Key
if err = client.RetryWithRelogin(cf.Context, tc, func() error {
key, err = tc.IssueUserCertsWithMFA(cf.Context, client.ReissueParams{
RouteToCluster: tc.SiteName,
@ -350,6 +357,16 @@ func databaseLogin(cf *CLIConf, tc *client.TeleportClient, route tlsca.RouteToDa
}
}
if route.Protocol == defaults.ProtocolOracle {
if err := generateDBLocalProxyCert(key, profile); err != nil {
return trace.Wrap(err)
}
err = oracle.GenerateClientConfiguration(key, route, profile)
if err != nil {
return trace.Wrap(err)
}
}
// Refresh the profile.
profile, err = tc.ProfileStatus()
if err != nil {
@ -589,7 +606,7 @@ func maybeStartLocalProxy(ctx context.Context, cf *CLIConf,
log.Debugf("Starting local proxy because: %v", strings.Join(requires.localProxyReasons, ", "))
}
listener, err := net.Listen("tcp", "localhost:0")
listener, err := createLocalProxyListener("localhost:0", route, profile)
if err != nil {
return nil, trace.Wrap(err)
}
@ -653,6 +670,25 @@ type localProxyConfig struct {
tunnel bool
}
func createLocalProxyListener(addr string, route *tlsca.RouteToDatabase, profile *client.ProfileStatus) (net.Listener, error) {
if route.Protocol == defaults.ProtocolOracle {
localCert, err := tls.LoadX509KeyPair(
profile.DatabaseLocalCAPath(),
profile.KeyPath(),
)
if err != nil {
return nil, trace.Wrap(err)
}
l, err := tls.Listen("tcp", addr, &tls.Config{
Certificates: []tls.Certificate{localCert},
ServerName: "localhost",
})
return l, trace.Wrap(err)
}
l, err := net.Listen("tcp", addr)
return l, trace.Wrap(err)
}
// prepareLocalProxyOptions created localProxyOpts needed to create local proxy from localProxyConfig.
func prepareLocalProxyOptions(arg *localProxyConfig) ([]alpnproxy.LocalProxyConfigOpt, error) {
if err := checkAndSetDBRouteDefaults(&arg.route); err != nil {
@ -861,9 +897,17 @@ func getDatabase(cf *CLIConf, tc *client.TeleportClient, dbName string) (types.D
func needDatabaseRelogin(cf *CLIConf, tc *client.TeleportClient, route *tlsca.RouteToDatabase, profile *client.ProfileStatus, requires *dbLocalProxyRequirement) (bool, error) {
if (requires.localProxy && requires.tunnel) || isLocalProxyTunnelRequested(cf) {
// We don't need to login if using a local proxy tunnel,
// because a local proxy tunnel will handle db login itself.
return false, nil
switch route.Protocol {
case defaults.ProtocolOracle:
// Oracle Protocol needs to generate a local configuration files.
// thus even is tunnel mode was requested the login flow should check
// if the Oracle client files should be updated.
default:
// We don't need to login if using a local proxy tunnel,
// because a local proxy tunnel will handle db login itself.
return false, nil
}
}
found := false
activeDatabases, err := profile.DatabasesForCluster(tc.SiteName)
@ -1121,7 +1165,9 @@ func getDBLocalProxyRequirement(tc *client.TeleportClient, route *tlsca.RouteToD
case defaults.ProtocolSnowflake,
defaults.ProtocolDynamoDB,
defaults.ProtocolSQLServer,
defaults.ProtocolCassandra:
defaults.ProtocolCassandra,
defaults.ProtocolOracle:
// Some protocols only work in the local tunnel mode.
out.addLocalProxyWithTunnel(formatDBProtocolReason(route.Protocol))
case defaults.ProtocolMySQL:

View file

@ -661,6 +661,12 @@ func TestFormatDatabaseConnectArgs(t *testing.T) {
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolDynamoDB, ServiceName: "svc"},
wantFlags: []string{"--db-user=<user>", "svc"},
},
{
name: "match user and db name, oracle protocol",
cluster: "",
route: tlsca.RouteToDatabase{Protocol: defaults.ProtocolOracle, ServiceName: "svc"},
wantFlags: []string{"--db-user=<user>", "--db-name=<name>", "svc"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

View file

@ -38,6 +38,7 @@ import (
"github.com/gravitational/teleport/lib/kube/kubeconfig"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
"github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/utils"
)
type proxyKubeCommand struct {
@ -257,7 +258,7 @@ func (k *kubeLocalProxy) Start(ctx context.Context) error {
// Close removes the temporary kubeconfig and closes the listeners.
func (k *kubeLocalProxy) Close() error {
removeFileIfExist(k.KubeConfigPath())
utils.RemoveFileIfExist(k.KubeConfigPath())
return trace.NewAggregate(k.forwardProxy.Close(), k.localProxy.Close())
}

View file

@ -19,6 +19,7 @@ package main
import (
"context"
"crypto/tls"
"crypto/x509/pkix"
"fmt"
"io"
"net"
@ -35,6 +36,7 @@ import (
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/client/webclient"
"github.com/gravitational/teleport/api/constants"
@ -46,6 +48,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/srv/alpnproxy"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)
@ -408,10 +411,12 @@ func onProxyCommandDB(cf *CLIConf) error {
randomPort = false
addr = fmt.Sprintf("127.0.0.1:%s", cf.LocalProxyPort)
}
listener, err := net.Listen("tcp", addr)
listener, err := createLocalProxyListener(addr, route, profile)
if err != nil {
return trace.Wrap(err)
}
defer func() {
if err := listener.Close(); err != nil {
log.WithError(err).Warnf("Failed to close listener.")
@ -473,7 +478,7 @@ func onProxyCommandDB(cf *CLIConf) error {
"randomPort": randomPort,
}
tmpl := chooseProxyCommandTemplate(templateArgs, commands)
tmpl := chooseProxyCommandTemplate(templateArgs, commands, route.Protocol)
err = tmpl.Execute(os.Stdout, templateArgs)
if err != nil {
return trace.Wrap(err)
@ -519,10 +524,14 @@ type templateCommandItem struct {
Command string
}
func chooseProxyCommandTemplate(templateArgs map[string]any, commands []dbcmd.CommandAlternative) *template.Template {
func chooseProxyCommandTemplate(templateArgs map[string]any, commands []dbcmd.CommandAlternative, protocol string) *template.Template {
// there is only one command, use plain template.
if len(commands) == 1 {
templateArgs["command"] = formatCommand(commands[0].Command)
if protocol == defaults.ProtocolOracle {
templateArgs["args"] = commands[0].Command.Args
return dbProxyOracleAuthTpl
}
return dbProxyAuthTpl
}
@ -821,6 +830,32 @@ func makeBasicLocalProxyConfig(cf *CLIConf, tc *libclient.TeleportClient, listen
}
}
func generateDBLocalProxyCert(key *libclient.Key, profile *libclient.ProfileStatus) error {
path := profile.DatabaseLocalCAPath()
if utils.FileExists(path) {
return nil
}
certPem, err := tlsca.GenerateSelfSignedCAWithConfig(tlsca.GenerateCAConfig{
Entity: pkix.Name{
CommonName: "localhost",
Organization: []string{"Teleport"},
},
Signer: key,
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP(defaults.Localhost)},
TTL: defaults.CATTL,
})
if err != nil {
return trace.Wrap(err)
}
if err := os.WriteFile(profile.DatabaseLocalCAPath(), certPem, teleport.FileMaskOwnerOnly); err != nil {
return trace.ConvertSystemError(err)
}
return nil
}
// dbProxyTpl is the message that gets printed to a user when a database proxy is started.
var dbProxyTpl = template.Must(template.New("").Parse(`Started DB proxy on {{.address}}
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
@ -831,6 +866,10 @@ Use following credentials to connect to the {{.database}} proxy:
key_file={{.key}}
`))
var templateFunctions = map[string]any{
"contains": strings.Contains,
}
// dbProxyAuthTpl is the message that's printed for an authenticated db proxy.
var dbProxyAuthTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
@ -840,6 +879,22 @@ Use the following command to connect to the database or to the address above usi
$ {{.command}}
`))
// dbProxyOracleAuthTpl is the message that's printed for an authenticated db proxy.
var dbProxyOracleAuthTpl = template.Must(template.New("").Funcs(templateFunctions).Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.
{{if .randomPort}}To avoid port randomization, you can choose the listening port using the --port flag.
{{end}}
Use the following command to connect to the Oracle database server using CLI:
$ {{.command}}
or using following Oracle JDBC connection string in order to connect with other GUI/CLI clients:
{{- range $val := .args}}
{{- if contains $val "jdbc:oracle:"}}
{{$val}}
{{- end}}
{{- end}}
`))
// dbProxyAuthMultiTpl is the message that's printed for an authenticated db proxy if there are multiple command options.
var dbProxyAuthMultiTpl = template.Must(template.New("").Parse(
`Started authenticated tunnel for the {{.type}} database "{{.database}}" in cluster "{{.cluster}}" on {{.address}}.

View file

@ -996,7 +996,7 @@ Use one of the following commands to connect to the database or to the address a
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
templateArgs := map[string]any{}
tpl := chooseProxyCommandTemplate(templateArgs, tt.commands)
tpl := chooseProxyCommandTemplate(templateArgs, tt.commands, "")
require.Equal(t, tt.wantTemplate, tpl)
require.Equal(t, tt.wantTemplateArgs, templateArgs)