Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions client/v2/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,18 @@ type HTTPConfig struct {
// DialContext specifies the dial function for creating unencrypted TCP connections.
// If DialContext is nil then the transport dials using package net.
DialContext func(ctx context.Context, network, addr string) (net.Conn, error)

// DialTLSContext specifies the dial function for creating TLS connections for
// non-proxied HTTPS requests. The returned connection is assumed to have
// already completed its TLS handshake. It allows the TLS configuration to be
// resolved per connection instead of being fixed when the client is created.
//
// When DialTLSContext is set, TLSConfig is only used for proxied requests,
// which tunnel through the proxy and so do not use this function. Set both to
// keep proxied requests working.
//
// If DialTLSContext is nil then DialContext and TLSConfig are used.
DialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)
}

// BatchPointsConfig is the config data needed to create an instance of the BatchPoints struct.
Expand Down Expand Up @@ -125,8 +137,9 @@ func NewHTTPClient(conf HTTPConfig) (HTTPClient, error) {
TLSClientConfig: &tls.Config{
InsecureSkipVerify: conf.InsecureSkipVerify,
},
Proxy: conf.Proxy,
DialContext: conf.DialContext,
Proxy: conf.Proxy,
DialContext: conf.DialContext,
DialTLSContext: conf.DialTLSContext,
}
if conf.TLSConfig != nil {
// A supplied TLSConfig is used as-is, including its own
Expand Down
123 changes: 98 additions & 25 deletions cmd/influx/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package cli // import "github.com/influxdata/influxdb/cmd/influx/cli"
import (
"bytes"
"context"
"crypto/tls"
"encoding/csv"
"encoding/json"
"errors"
Expand All @@ -29,6 +30,7 @@ import (
fluxClient "github.com/influxdata/influxdb/flux/client"
v8 "github.com/influxdata/influxdb/importer/v8"
"github.com/influxdata/influxdb/models"
"github.com/influxdata/influxdb/pkg/tlsconfig"
"github.com/influxdata/influxql"
"github.com/peterh/liner"
"golang.org/x/term"
Expand All @@ -39,30 +41,39 @@ var ErrBlankCommand = errors.New("empty input")

// CommandLine holds CLI configuration and state.
type CommandLine struct {
Line *liner.State
URL url.URL
Host string
Port int
PathPrefix string
Database string
Type QueryLanguage
Ssl bool
RetentionPolicy string
ClientVersion string
ServerVersion string
Pretty bool // controls pretty print for json
Format string // controls the output format. Valid values are json, csv, or column
Execute string
ShowVersion bool
Import bool
Chunked bool
ChunkSize int
NodeID int
Quit chan struct{}
IgnoreSignals bool // Ignore signals normally caught by this process (used primarily for testing)
ForceTTY bool // Force the CLI to act as if it were connected to a TTY
osSignals chan os.Signal
historyFilePath string
Line *liner.State
URL url.URL
Host string
Port int
PathPrefix string
Database string
Type QueryLanguage
Ssl bool
ClientCert string // path to the client certificate presented for mutual TLS
ClientKey string // path to the client private key for mutual TLS
RootCA string // path to the CA bundle used to verify the server certificate
// InsecureCertificate ignores file permission checks when loading the client
// certificate and key.
InsecureCertificate bool
// IgnoreCertSanityChecks loads the client certificate even when it fails the
// sanity checks that decide whether it is usable for client authentication.
IgnoreCertSanityChecks bool
RetentionPolicy string
ClientVersion string
ServerVersion string
Pretty bool // controls pretty print for json
Format string // controls the output format. Valid values are json, csv, or column
Execute string
ShowVersion bool
Import bool
Chunked bool
ChunkSize int
NodeID int
Quit chan struct{}
IgnoreSignals bool // Ignore signals normally caught by this process (used primarily for testing)
ForceTTY bool // Force the CLI to act as if it were connected to a TTY
osSignals chan os.Signal
historyFilePath string

Client *client.Client
ClientConfig client.Config // Client config options.
Expand Down Expand Up @@ -118,6 +129,32 @@ func (c *CommandLine) Run() error {
c.ClientConfig.Password = os.Getenv("INFLUX_PASSWORD")
}

// Read TLS settings from the environment when the corresponding flag was not
// given, mirroring the username/password precedence above.
if c.ClientCert == "" {
c.ClientCert = os.Getenv("INFLUX_CERT")
}
if c.ClientKey == "" {
c.ClientKey = os.Getenv("INFLUX_KEY")
}
if c.RootCA == "" {
c.RootCA = os.Getenv("INFLUX_ROOT_CA")
}
if !c.InsecureCertificate {
c.InsecureCertificate, _ = strconv.ParseBool(os.Getenv("INFLUX_INSECURE_CERTIFICATE"))
}
if !c.IgnoreCertSanityChecks {
c.IgnoreCertSanityChecks, _ = strconv.ParseBool(os.Getenv("INFLUX_IGNORE_CERT_SANITY_CHECKS"))
}

// Build the mutual TLS config once, up front, so a bad certificate or key is
// reported before any connection is attempted.
tlsConfig, err := c.buildClientTLSConfig()
if err != nil {
return err
}
c.ClientConfig.TLS = tlsConfig

addr := fmt.Sprintf("%s:%d/%s", c.Host, c.Port, c.PathPrefix)
url, err := client.ParseConnectionString(addr, c.Ssl)
if err != nil {
Expand Down Expand Up @@ -146,7 +183,7 @@ func (c *CommandLine) Run() error {
}
c.ClientConfig.UnsafeSsl = false
}
return fmt.Errorf("Failed to connect to %s: %s\n%s", c.Client.Addr(), err.Error(), msg)
return fmt.Errorf("Failed to connect to %s: %w\n%s", c.Client.Addr(), err, msg)
}

// Modify precision.
Expand Down Expand Up @@ -337,6 +374,42 @@ func (c *CommandLine) ParseCommand(cmd string) error {
return ErrBlankCommand
}

// buildClientTLSConfig builds the *tls.Config used for mutual TLS from the
// configured client certificate, private key, and root CA. It returns nil when
// none of them are set, leaving the client's default TLS behavior unchanged.
//
// InsecureSkipVerify is intentionally not set here; the client applies it from
// the -unsafeSsl flag, which the SSL-detection retry logic in Run toggles after
// this config has been built.
func (c *CommandLine) buildClientTLSConfig() (*tls.Config, error) {
if c.ClientCert == "" && c.ClientKey == "" && c.RootCA == "" {
return nil, nil
}

var rootCA *tlsconfig.CAConfig
if c.RootCA != "" {
rootCA = &tlsconfig.CAConfig{Paths: []string{c.RootCA}}
}

// The influx CLI is a one-shot client, so the certificate monitor is never
// started; it exists only because the config manager requires one to build
// its client certificate loader.
monitor := tlsconfig.NewTLSCertMonitor()
cm, err := tlsconfig.NewClientTLSConfigManager(
monitor,
tlsconfig.WithUsage("influx"),
tlsconfig.WithUseTLS(true),
tlsconfig.WithClientCertificate(c.ClientCert, c.ClientKey),
tlsconfig.WithRootCA(rootCA),
tlsconfig.WithIgnoreFilePermissions(c.InsecureCertificate),
tlsconfig.WithIgnoreSanityChecks(c.IgnoreCertSanityChecks),
)
if err != nil {
return nil, err
}
return cm.TLSConfig(), nil
}

// Connect connects to a server.
func (c *CommandLine) Connect(cmd string) error {
// normalize cmd
Expand Down
118 changes: 118 additions & 0 deletions cmd/influx/cli/cli_mtls_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package cli_test

import (
"crypto/tls"
"crypto/x509"
"net"
"net/http"
"net/http/httptest"
"net/url"
"os"
"strconv"
"testing"

"github.com/influxdata/influxdb/cmd/influx/cli"
"github.com/influxdata/influxdb/pkg/testing/selfsigned"
"github.com/stretchr/testify/require"
)

// mtlsTestServer starts an HTTPS test server that requires and verifies a client
// certificate signed by cert's CA. It returns the host and port the CLI should
// dial.
func mtlsTestServer(t *testing.T, cert *selfsigned.Cert) (host string, port int) {
t.Helper()

serverPair, err := tls.LoadX509KeyPair(cert.CertPath, cert.KeyPath)
require.NoError(t, err)

caPEM, err := os.ReadFile(cert.CACertPath)
require.NoError(t, err)
pool := x509.NewCertPool()
require.True(t, pool.AppendCertsFromPEM(caPEM), "failed to append CA certificate")

ts := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Influxdb-Version", SERVER_VERSION)
}))
ts.TLS = &tls.Config{
Certificates: []tls.Certificate{serverPair},
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: pool,
}
ts.StartTLS()
t.Cleanup(ts.Close)

u, err := url.Parse(ts.URL)
require.NoError(t, err)
h, p, err := net.SplitHostPort(u.Host)
require.NoError(t, err)
port, err = strconv.Atoi(p)
require.NoError(t, err)
return h, port
}

// TestRunCLI_MutualTLS verifies the CLI presents its client certificate and
// verifies the server against the configured root CA.
func TestRunCLI_MutualTLS(t *testing.T) {
cert := selfsigned.NewSelfSignedCert(t)
host, port := mtlsTestServer(t, cert)

c := cli.New(CLIENT_VERSION)
c.Host = host
c.Port = port
c.Ssl = true
c.ClientCert = cert.CertPath
c.ClientKey = cert.KeyPath
c.RootCA = cert.CACertPath
c.Format = "column"
c.ClientConfig.Precision = "ns"
c.Execute = "SHOW DATABASES"
c.IgnoreSignals = true
c.ForceTTY = true

require.NoError(t, c.Run())
require.Equal(t, SERVER_VERSION, c.ServerVersion)
}

// TestRunCLI_MutualTLS_MissingClientCert verifies the connection is refused when
// the CLI trusts the server but presents no client certificate.
func TestRunCLI_MutualTLS_MissingClientCert(t *testing.T) {
cert := selfsigned.NewSelfSignedCert(t)
host, port := mtlsTestServer(t, cert)

c := cli.New(CLIENT_VERSION)
c.Host = host
c.Port = port
c.Ssl = true
c.RootCA = cert.CACertPath // trust the server, but present no client cert
c.Execute = "SHOW DATABASES"
c.IgnoreSignals = true
c.ForceTTY = true

// The server aborts the handshake because no client certificate was sent.
require.ErrorContains(t, c.Run(), "certificate required")
}

// TestRunCLI_MutualTLS_UntrustedServer verifies the server certificate is
// rejected when no root CA that signed it is configured.
func TestRunCLI_MutualTLS_UntrustedServer(t *testing.T) {
cert := selfsigned.NewSelfSignedCert(t)
host, port := mtlsTestServer(t, cert)

c := cli.New(CLIENT_VERSION)
c.Host = host
c.Port = port
c.Ssl = true
c.ClientCert = cert.CertPath
c.ClientKey = cert.KeyPath
// No RootCA: the self-signed server is not in the system pool.
c.Execute = "SHOW DATABASES"
c.IgnoreSignals = true
c.ForceTTY = true

// The server certificate cannot be verified against any trusted CA, which
// surfaces as a TLS certificate-verification error in the wrapped chain.
err := c.Run()
var certErr *tls.CertificateVerificationError
require.ErrorAs(t, err, &certErr)
require.ErrorContains(t, err, "certificate signed by unknown authority")
}
15 changes: 15 additions & 0 deletions cmd/influx/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ func main() {
fs.Var(&c.Type, "type", "query language for executing commands or invoking the REPL: influxql, flux")
fs.BoolVar(&c.Ssl, "ssl", false, "Use https for connecting to cluster.")
fs.BoolVar(&c.ClientConfig.UnsafeSsl, "unsafeSsl", false, "Set this when connecting to the cluster using https and not use SSL verification.")
fs.StringVar(&c.ClientCert, "cert", "", "Path to the client certificate file (PEM) presented for mutual TLS. Also settable via INFLUX_CERT.")
fs.StringVar(&c.ClientKey, "key", "", "Path to the client private key file (PEM) for mutual TLS. Also settable via INFLUX_KEY.")
fs.StringVar(&c.RootCA, "root-ca", "", "Path to the CA bundle (PEM) used to verify the server certificate. Also settable via INFLUX_ROOT_CA.")
fs.BoolVar(&c.InsecureCertificate, "insecure-certificate", false, "Ignore file permission checks when loading the client certificate and key. Also settable via INFLUX_INSECURE_CERTIFICATE.")
fs.BoolVar(&c.IgnoreCertSanityChecks, "ignore-cert-sanity-checks", false, "Load the client certificate even if it fails client-authentication sanity checks. Also settable via INFLUX_IGNORE_CERT_SANITY_CHECKS.")
fs.StringVar(&c.Format, "format", defaultFormat, "Format specifies the format of the server responses: json, csv, or column.")
fs.StringVar(&c.ClientConfig.Precision, "precision", defaultPrecision, "Precision specifies the format of the timestamp: rfc3339,h,m,s,ms,u or ns.")
fs.StringVar(&c.ClientConfig.WriteConsistency, "consistency", "all", "Set write consistency level: any, one, quorum, or all.")
Expand Down Expand Up @@ -84,6 +89,16 @@ func main() {
Use https for requests.
-unsafeSsl
Set this when connecting to the cluster using https and not use SSL verification.
-cert 'path'
Path to the client certificate file (PEM) presented for mutual TLS.
-key 'path'
Path to the client private key file (PEM) for mutual TLS.
-root-ca 'path'
Path to the CA bundle (PEM) used to verify the server certificate.
-insecure-certificate
Ignore file permission checks when loading the client certificate and key.
-ignore-cert-sanity-checks
Load the client certificate even if it fails client-authentication sanity checks.
-execute 'command'
Execute command and quit.
-type 'influxql|flux'
Expand Down
Loading
Loading