mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Add Support for Oracle protocol (#23227)
This commit is contained in:
parent
3fbe7f7ff4
commit
0f3c14e0f9
|
@ -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)
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
2
go.sum
|
@ -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=
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 {
|
||||
|
|
115
lib/client/db/oracle/config.go
Normal file
115
lib/client/db/oracle/config.go
Normal 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
|
||||
}
|
144
lib/client/db/oracle/oracle.go
Normal file
144
lib/client/db/oracle/oracle.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
//
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
35
lib/srv/db/common/enterprise/enterprise.go
Normal file
35
lib/srv/db/common/enterprise/enterprise.go
Normal 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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
||||
|
|
|
@ -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}}.
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in a new issue