Skip to content

Commit 96f527f

Browse files
committed
connect: add SSL support
The patch adds options to configure traffic encryption[1]: --sslkeyfile - a path to a private SSL key file; --sslcerfile - a path to an SSL certificate file; --sslcafile - a path to a trusted certificate authorities (CA) file; --sslciphers - colon-separated (:) list of SSL cipher suites the connection can use; 1. https://www.tarantool.io/en/enterprise_doc/security/#configuration Part of #308
1 parent fc7be12 commit 96f527f

17 files changed

+520
-80
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1515
as control socket, pid file and log file. This option affects only single instance applications.
1616
- An ability to set different directories for WAL, vinyl and snapshots artifacts.
1717
- ``tt instances`` command to print a list of enabled applications.
18+
- SSL options for ``tt connect`` command.
1819

1920
### Changed
2021

cli/cmd/connect.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ var (
2929
connectPassword string
3030
connectFile string
3131
connectLanguage string
32+
connectSslKeyFile string
33+
connectSslCertFile string
34+
connectSslCaFile string
35+
connectSslCiphers string
3236
connectInteractive bool
3337
)
3438

@@ -58,6 +62,16 @@ func NewConnectCmd() *cobra.Command {
5862
`file to read the script for evaluation. "-" - read the script from stdin`)
5963
connectCmd.Flags().StringVarP(&connectLanguage, "language", "l",
6064
connect.DefaultLanguage.String(), `language: lua or sql`)
65+
connectCmd.Flags().StringVarP(&connectSslKeyFile, "sslkeyfile", "",
66+
connect.DefaultLanguage.String(), `path to a private SSL key file`)
67+
connectCmd.Flags().StringVarP(&connectSslCertFile, "sslcertfile", "",
68+
connect.DefaultLanguage.String(), `path to an SSL certificate file`)
69+
connectCmd.Flags().StringVarP(&connectSslCaFile, "sslcafile", "",
70+
connect.DefaultLanguage.String(),
71+
`path to a trusted certificate authorities (CA) file`)
72+
connectCmd.Flags().StringVarP(&connectSslCiphers, "sslciphers", "",
73+
connect.DefaultLanguage.String(),
74+
`colon-separated (:) list of SSL cipher suites the connection`)
6175
connectCmd.Flags().BoolVarP(&connectInteractive, "interactive", "i",
6276
false, `enter interactive mode after executing 'FILE'`)
6377

@@ -170,6 +184,10 @@ func internalConnectModule(cmdCtx *cmdcontext.CmdCtx, args []string) error {
170184
Username: connectUser,
171185
Password: connectPassword,
172186
SrcFile: connectFile,
187+
SslKeyFile: connectSslKeyFile,
188+
SslCertFile: connectSslCertFile,
189+
SslCaFile: connectSslCaFile,
190+
SslCiphers: connectSslCiphers,
173191
Interactive: connectInteractive,
174192
}
175193

cli/connect/connect.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ type ConnectCtx struct {
2222
SrcFile string
2323
// Language to use for execution.
2424
Language Language
25+
// SslKeyFile is a path to a private SSL key file.
26+
SslKeyFile string
27+
// SslCertFile is a path to an SSL certificate file.
28+
SslCertFile string
29+
// SslCaFile is a path to a trusted certificate authorities (CA) file.
30+
SslCaFile string
31+
// SslCiphers is a colon-separated (:) list of SSL cipher suites the
32+
// connection can use.
33+
SslCiphers string
2534
// Interactive mode is used.
2635
Interactive bool
2736
}
@@ -34,7 +43,13 @@ const (
3443
func getConnOpts(connString string, connCtx ConnectCtx) connector.ConnectOpts {
3544
username := connCtx.Username
3645
password := connCtx.Password
37-
return connector.MakeConnectOpts(connString, username, password)
46+
ssl := connector.SslOpts{
47+
KeyFile: connCtx.SslKeyFile,
48+
CertFile: connCtx.SslCertFile,
49+
CaFile: connCtx.SslCaFile,
50+
Ciphers: connCtx.SslCiphers,
51+
}
52+
return connector.MakeConnectOpts(connString, username, password, ssl)
3853
}
3954

4055
// getEvalCmd returns a command from the input source (file or stdin).

cli/connector/connector.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,22 @@ func Connect(opts ConnectOpts) (Connector, error) {
7474
// Set a deadline for the greeting.
7575
greetingConn.SetReadDeadline(time.Now().Add(greetingOperationTimeout))
7676

77-
// Detect protocol.
77+
// Detect transport and protocol.
78+
ssl := opts.Ssl.KeyFile != "" || opts.Ssl.CertFile != "" ||
79+
opts.Ssl.CaFile != "" || opts.Ssl.Ciphers != ""
80+
transport := ""
7881
protocol, err := GetProtocol(greetingConn)
7982
if err != nil {
80-
return nil, fmt.Errorf("failed to get protocol: %s", err)
83+
if ssl {
84+
protocol = BinaryProtocol
85+
transport = "ssl"
86+
} else {
87+
return nil, fmt.Errorf("failed to get protocol: %s", err)
88+
}
89+
} else if ssl {
90+
greetingConn.Close()
91+
errMsg := "unencrypted connection established, but encryption required"
92+
return nil, fmt.Errorf(errMsg)
8193
}
8294

8395
// Reset the deadline. From the SetDeadline doc:
@@ -95,6 +107,8 @@ func Connect(opts ConnectOpts) (Connector, error) {
95107
conn, err := tarantool.Connect(addr, tarantool.Opts{
96108
User: opts.Username,
97109
Pass: opts.Password,
110+
Transport: transport,
111+
Ssl: tarantool.SslOpts(opts.Ssl),
98112
SkipSchema: true, // We don't need a schema for eval requests.
99113
})
100114
if err != nil {

cli/connector/integration_test.go

Lines changed: 106 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// +build integration
1+
//go:build integration
22

33
package connector_test
44

@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
1314
"github.com/tarantool/go-tarantool"
1415
"github.com/tarantool/go-tarantool/test_helpers"
1516

@@ -18,13 +19,20 @@ import (
1819

1920
const workDir = "work_dir"
2021
const server = "127.0.0.1:3013"
22+
const serverTls = "127.0.0.1:3014"
2123
const console = workDir + "/" + "console.control"
2224

25+
var tarantoolEe bool
2326
var opts = tarantool.Opts{
2427
Timeout: 500 * time.Millisecond,
2528
User: "test",
2629
Pass: "password",
2730
}
31+
var sslOpts = SslOpts{
32+
KeyFile: "testdata/localhost.key",
33+
CertFile: "testdata/localhost.crt",
34+
CaFile: "testdata/ca.crt",
35+
}
2836

2937
func textConnectWithValidation(t *testing.T) *TextConnector {
3038
t.Helper()
@@ -167,12 +175,44 @@ func TestBinaryConnector_Eval_pushCallback(t *testing.T) {
167175

168176
func TestConnect_binary(t *testing.T) {
169177
conn, err := Connect(ConnectOpts{
170-
Network: "tcp",
171-
Address: server,
178+
Network: "tcp",
179+
Address: server,
172180
Username: "test",
173181
Password: "password",
174182
})
183+
require.NoError(t, err)
184+
defer conn.Close()
185+
186+
eval := "return 'hello', 'world'"
187+
ret, err := conn.Eval(eval, []interface{}{}, RequestOpts{})
175188
assert.NoError(t, err)
189+
assert.Equal(t, []interface{}{"hello", "world"}, ret)
190+
}
191+
192+
func TestConnect_binaryTlsToNoTls(t *testing.T) {
193+
_, err := Connect(ConnectOpts{
194+
Network: "tcp",
195+
Address: server,
196+
Username: "test",
197+
Password: "password",
198+
Ssl: sslOpts,
199+
})
200+
expected := "unencrypted connection established, but encryption required"
201+
require.ErrorContains(t, err, expected)
202+
}
203+
204+
func TestConnect_binaryTlsToTls(t *testing.T) {
205+
if !tarantoolEe {
206+
t.Skip("Only for Tarantool Enterprise.")
207+
}
208+
conn, err := Connect(ConnectOpts{
209+
Network: "tcp",
210+
Address: serverTls,
211+
Username: "test",
212+
Password: "password",
213+
Ssl: sslOpts,
214+
})
215+
require.NoError(t, err)
176216
defer conn.Close()
177217

178218
eval := "return 'hello', 'world'"
@@ -186,7 +226,7 @@ func TestConnect_text(t *testing.T) {
186226
Network: "unix",
187227
Address: console,
188228
})
189-
assert.NoError(t, err)
229+
require.NoError(t, err)
190230
defer conn.Close()
191231

192232
eval := "return 'hello', 'world'"
@@ -195,23 +235,76 @@ func TestConnect_text(t *testing.T) {
195235
assert.Equal(t, []interface{}{"hello", "world"}, ret)
196236
}
197237

238+
func TestConnect_textTls(t *testing.T) {
239+
_, err := Connect(ConnectOpts{
240+
Network: "unix",
241+
Address: console,
242+
Ssl: sslOpts,
243+
})
244+
expected := "unencrypted connection established, but encryption required"
245+
require.ErrorContains(t, err, expected)
246+
}
247+
198248
func runTestMain(m *testing.M) int {
199249
inst, err := test_helpers.StartTarantool(test_helpers.StartOpts{
200-
InitScript: "testdata/config.lua",
201-
Listen: server,
202-
WorkDir: workDir,
203-
User: opts.User,
204-
Pass: opts.Pass,
205-
WaitStart: 100 * time.Millisecond,
206-
ConnectRetry: 3,
207-
RetryTimeout: 500 * time.Millisecond,
250+
InitScript: "testdata/config.lua",
251+
Listen: server,
252+
WorkDir: workDir,
253+
User: opts.User,
254+
Pass: opts.Pass,
255+
WaitStart: 100 * time.Millisecond,
256+
ConnectRetry: 3,
257+
RetryTimeout: 500 * time.Millisecond,
208258
})
209259
defer test_helpers.StopTarantoolWithCleanup(inst)
210-
211260
if err != nil {
212261
log.Fatalf("Failed to prepare test tarantool: %s", err)
213262
}
214263

264+
conn, err := tarantool.Connect(server, opts)
265+
if err != nil {
266+
log.Fatalf("Failed to check tarantool version: %s", err)
267+
}
268+
req := tarantool.NewEvalRequest("return box.info.package")
269+
resp, err := conn.Do(req).Get()
270+
conn.Close()
271+
272+
if err != nil {
273+
log.Fatalf("Failed to get box.info.package: %s", err)
274+
}
275+
276+
if len(resp.Data) > 0 {
277+
if pack, ok := resp.Data[0].(string); ok {
278+
tarantoolEe = pack == "Tarantool Enterprise"
279+
}
280+
}
281+
282+
if tarantoolEe {
283+
// Try to start Tarantool instance with TLS.
284+
listen := serverTls + "?transport=ssl&" +
285+
"ssl_key_file=testdata/localhost.key&" +
286+
"ssl_cert_file=testdata/localhost.crt&" +
287+
"ssl_ca_file=testdata/ca.crt"
288+
inst, err = test_helpers.StartTarantool(test_helpers.StartOpts{
289+
InitScript: "testdata/config.lua",
290+
Listen: listen,
291+
SslCertsDir: "testdata",
292+
ClientServer: serverTls,
293+
ClientTransport: "ssl",
294+
ClientSsl: tarantool.SslOpts(sslOpts),
295+
WorkDir: workDir,
296+
User: opts.User,
297+
Pass: opts.Pass,
298+
WaitStart: 100 * time.Millisecond,
299+
ConnectRetry: 3,
300+
RetryTimeout: 500 * time.Millisecond,
301+
})
302+
if err != nil {
303+
log.Fatalf("Failed to prepare test tarantool with TLS: %s", err)
304+
}
305+
defer test_helpers.StopTarantoolWithCleanup(inst)
306+
}
307+
215308
return m.Run()
216309
}
217310

cli/connector/opts.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,33 @@ type ConnectOpts struct {
2020
Username string
2121
// Password of the user.
2222
Password string
23+
// Ssl options for a connection.
24+
Ssl SslOpts
25+
}
26+
27+
// SslOpts is a way to configure SSL connection.
28+
type SslOpts struct {
29+
// KeyFile is a path to a private SSL key file.
30+
KeyFile string
31+
// CertFile is a path to an SSL certificate file.
32+
CertFile string
33+
// CaFile is a path to a trusted certificate authorities (CA) file.
34+
CaFile string
35+
// Ciphers is a colon-separated (:) list of SSL cipher suites the
36+
// connection can use.
37+
Ciphers string
2338
}
2439

2540
// MakeConnectOpts creates a new connection options object according to the
2641
// arguments passed. An username and a password values from the connection
2742
// string are used only if the username and password from the arguments are
2843
// empty.
29-
func MakeConnectOpts(connString, username, password string) ConnectOpts {
44+
func MakeConnectOpts(connString, username, password string,
45+
ssl SslOpts) ConnectOpts {
3046
connOpts := ConnectOpts{
3147
Username: username,
3248
Password: password,
49+
Ssl: ssl,
3350
}
3451

3552
connStringParts := strings.SplitN(connString, "@", 2)

0 commit comments

Comments
 (0)