Skip to content

Commit 6b66dc3

Browse files
markphelpsCopilot
andauthored
feat: add TLS configuration support to Go SDK (#1138)
* feat: support custom tls config in Go Signed-off-by: Mark Phelps <[email protected]> * chore: simplify tls logic Signed-off-by: Mark Phelps <[email protected]> * chore: refactor to just use std lib tls.Config Signed-off-by: Mark Phelps <[email protected]> * chore: nil check Signed-off-by: Mark Phelps <[email protected]> * chore: mv tls.go into config.go Signed-off-by: Mark Phelps <[email protected]> * chore: skip verify for invalid auth test Signed-off-by: Mark Phelps <[email protected]> * chore: Update flipt-client-go/config.go Co-authored-by: Copilot <[email protected]> Signed-off-by: Mark Phelps <[email protected]> * chore: rename tlsconfig to be go like Signed-off-by: Mark Phelps <[email protected]> * chore: simplify logic Signed-off-by: Mark Phelps <[email protected]> * chore: add docs on order of operations Signed-off-by: Mark Phelps <[email protected]> --------- Signed-off-by: Mark Phelps <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 6f41571 commit 6b66dc3

File tree

4 files changed

+198
-5
lines changed

4 files changed

+198
-5
lines changed

flipt-client-go/README.md

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ The `NewClient` constructor accepts a variadic number of `Option` functions that
125125
- `WithReference`: The [reference](https://docs.flipt.io/guides/user/using-references) to use when fetching flag state. If not provided, reference will not be used.
126126
- `WithFetchMode`: The fetch mode to use when fetching flag state. If not provided, the client will default to polling.
127127
- `WithErrorStrategy`: The error strategy to use when fetching flag state. If not provided, the client will default to `Fail`. See the [Error Strategies](#error-strategies) section for more information.
128+
- `WithTLSConfig`: The TLS configuration for connecting to servers with custom certificates. See [TLS Configuration](#tls-configuration). Note: if used with `WithHTTPClient`, this should be called after setting the HTTP client.
128129

129130
### Authentication
130131

@@ -134,6 +135,134 @@ The `Client` supports the following authentication strategies:
134135
- [Client Token Authentication](https://docs.flipt.io/authentication/using-tokens)
135136
- [JWT Authentication](https://docs.flipt.io/authentication/using-jwts)
136137

138+
### TLS Configuration
139+
140+
The `Client` supports configuring TLS settings for secure connections to Flipt servers using the standard library `tls.Config`. This provides maximum flexibility for:
141+
142+
- Connecting to Flipt servers with self-signed certificates
143+
- Using custom Certificate Authorities (CAs)
144+
- Implementing mutual TLS authentication
145+
- Testing with insecure connections (development only)
146+
147+
#### Basic TLS with Custom CA Certificate
148+
149+
```go
150+
package main
151+
152+
import (
153+
"context"
154+
"crypto/tls"
155+
"crypto/x509"
156+
"log"
157+
"os"
158+
159+
flipt "go.flipt.io/flipt-client"
160+
)
161+
162+
func main() {
163+
ctx := context.Background()
164+
165+
// Load CA certificate
166+
caCert, err := os.ReadFile("/path/to/ca.pem")
167+
if err != nil {
168+
log.Fatal(err)
169+
}
170+
171+
caCertPool := x509.NewCertPool()
172+
caCertPool.AppendCertsFromPEM(caCert)
173+
174+
tlsConfig := &tls.Config{
175+
RootCAs: caCertPool,
176+
}
177+
178+
client, err := flipt.NewClient(
179+
ctx,
180+
flipt.WithURL("https://flipt.example.com"),
181+
flipt.WithTLSConfig(tlsConfig),
182+
flipt.WithClientTokenAuthentication("your-token"),
183+
)
184+
if err != nil {
185+
log.Fatal(err)
186+
}
187+
defer client.Close(ctx)
188+
}
189+
```
190+
191+
#### Mutual TLS Authentication
192+
193+
```go
194+
// Load client certificate and key
195+
clientCert, err := tls.LoadX509KeyPair("/path/to/client.pem", "/path/to/client.key")
196+
if err != nil {
197+
log.Fatal(err)
198+
}
199+
200+
// Load CA certificate
201+
caCert, err := os.ReadFile("/path/to/ca.pem")
202+
if err != nil {
203+
log.Fatal(err)
204+
}
205+
206+
caCertPool := x509.NewCertPool()
207+
caCertPool.AppendCertsFromPEM(caCert)
208+
209+
tlsConfig := &tls.Config{
210+
Certificates: []tls.Certificate{clientCert},
211+
RootCAs: caCertPool,
212+
}
213+
214+
client, err := flipt.NewClient(
215+
ctx,
216+
flipt.WithURL("https://flipt.example.com"),
217+
flipt.WithTLSConfig(tlsConfig),
218+
flipt.WithClientTokenAuthentication("your-token"),
219+
)
220+
```
221+
222+
#### Development Mode (Insecure)
223+
224+
**⚠️ WARNING: Only use this in development environments!**
225+
226+
```go
227+
// Skip certificate verification (NOT for production)
228+
tlsConfig := &tls.Config{
229+
InsecureSkipVerify: true,
230+
}
231+
232+
client, err := flipt.NewClient(
233+
ctx,
234+
flipt.WithURL("https://localhost:8443"),
235+
flipt.WithTLSConfig(tlsConfig),
236+
flipt.WithClientTokenAuthentication("your-token"),
237+
)
238+
```
239+
240+
#### Advanced TLS Configuration
241+
242+
Since the client accepts a standard `tls.Config`, you have access to all TLS configuration options:
243+
244+
```go
245+
tlsConfig := &tls.Config{
246+
// Custom CA certificates
247+
RootCAs: caCertPool,
248+
249+
// Client certificates for mutual TLS
250+
Certificates: []tls.Certificate{clientCert},
251+
252+
// Minimum TLS version
253+
MinVersion: tls.VersionTLS12,
254+
255+
// Custom cipher suites
256+
CipherSuites: []uint16{
257+
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
258+
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
259+
},
260+
261+
// Server name for SNI
262+
ServerName: "flipt.example.com",
263+
}
264+
```
265+
137266
### Error Strategies
138267

139268
The `Client` supports the following error strategies:

flipt-client-go/client_test.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ package flipt_test
22

33
import (
44
"context"
5+
"crypto/tls"
6+
"crypto/x509"
57
"os"
8+
"strings"
69
"testing"
710

811
"github.com/stretchr/testify/require"
@@ -34,6 +37,27 @@ func (s *ClientTestSuite) SetupSuite() {
3437
opts = append(opts, flipt.WithClientTokenAuthentication(s.authToken))
3538
}
3639

40+
// Configure TLS if HTTPS URL is provided
41+
if s.fliptURL != "" && strings.HasPrefix(s.fliptURL, "https://") {
42+
caCertPath := os.Getenv("FLIPT_CA_CERT_PATH")
43+
if caCertPath != "" {
44+
caCertData, err := os.ReadFile(caCertPath)
45+
require.NoError(s.T(), err)
46+
47+
caCertPool := x509.NewCertPool()
48+
caCertPool.AppendCertsFromPEM(caCertData)
49+
50+
opts = append(opts, flipt.WithTLSConfig(&tls.Config{
51+
RootCAs: caCertPool,
52+
}))
53+
} else {
54+
// Fallback to insecure for local testing
55+
opts = append(opts, flipt.WithTLSConfig(&tls.Config{
56+
InsecureSkipVerify: true,
57+
}))
58+
}
59+
}
60+
3761
var err error
3862
s.client, err = flipt.NewClient(context.TODO(), opts...)
3963
require.NoError(s.T(), err)
@@ -46,7 +70,9 @@ func (s *ClientTestSuite) TearDownSuite() {
4670
func (s *ClientTestSuite) TestInvalidAuthentication() {
4771
_, err := flipt.NewClient(context.TODO(),
4872
flipt.WithURL(s.fliptURL),
49-
flipt.WithClientTokenAuthentication("invalid"))
73+
flipt.WithClientTokenAuthentication("invalid"),
74+
flipt.WithTLSConfig(&tls.Config{InsecureSkipVerify: true}),
75+
)
5076
s.EqualError(err, "failed to fetch initial state: unexpected status code: 401")
5177
}
5278

flipt-client-go/config.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package flipt
22

33
import (
4+
"crypto/tls"
45
"fmt"
56
"net"
67
"net/http"
@@ -91,6 +92,41 @@ func WithRequestTimeout(timeout time.Duration) Option {
9192
}
9293
}
9394

95+
// WithTLSConfig sets the TLS configuration for the client using the standard library tls.Config.
96+
// This provides maximum flexibility for configuring TLS settings including custom CAs,
97+
// mutual TLS authentication, and certificate verification options.
98+
// Note: if used with WithHTTPClient, this should be called after setting the HTTP client.
99+
func WithTLSConfig(tlsConfig *tls.Config) Option {
100+
return func(cfg *config) {
101+
if tlsConfig == nil {
102+
return
103+
}
104+
105+
// Create a new HTTP client based on the one provided or the default one
106+
var (
107+
transport = defaultHTTPClient.Transport.(*http.Transport).Clone()
108+
timeout = defaultHTTPClient.Timeout
109+
)
110+
111+
if cfg.HTTPClient != nil {
112+
timeout = cfg.HTTPClient.Timeout
113+
if cfg.HTTPClient.Transport != nil {
114+
if t, ok := cfg.HTTPClient.Transport.(*http.Transport); ok {
115+
transport = t.Clone()
116+
}
117+
}
118+
}
119+
120+
// Apply the provided TLS config
121+
transport.TLSClientConfig = tlsConfig
122+
123+
cfg.HTTPClient = &http.Client{
124+
Transport: transport,
125+
Timeout: timeout,
126+
}
127+
}
128+
}
129+
94130
// defaultHTTPClient is the default HTTP client used by the client.
95131
// It is used to make requests to the upstream Flipt instance and is configured to be best compatible with streaming mode.
96132
var defaultHTTPClient = &http.Client{

test/main.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,9 @@ var (
7575
}
7676

7777
goVersions = []containerConfig{
78-
{base: "golang:1.23-bookworm", setup: []string{"apt-get update", "apt-get install -y build-essential"}},
79-
{base: "golang:1.23-bullseye", setup: []string{"apt-get update", "apt-get install -y build-essential"}},
80-
{base: "golang:1.23-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}},
78+
{base: "golang:1.23-bookworm", setup: []string{"apt-get update", "apt-get install -y build-essential"}, useHTTPS: true},
79+
{base: "golang:1.23-bullseye", setup: []string{"apt-get update", "apt-get install -y build-essential"}, useHTTPS: true},
80+
{base: "golang:1.23-alpine", setup: []string{"apk update", "apk add --no-cache build-base"}, useHTTPS: true},
8181
}
8282

8383
rubyVersions = []containerConfig{
@@ -431,8 +431,10 @@ func goTests(ctx context.Context, root *dagger.Container, t *testCase) error {
431431
WithWorkdir("/src").
432432
WithDirectory("/src", t.hostDir.Directory("flipt-client-go")).
433433
WithFile("/src/ext/flipt_engine_wasm.wasm", t.engine.File(wasmFile)).
434+
WithDirectory("/src/test/fixtures/tls", t.hostDir.Directory("test/fixtures/tls")).
434435
WithServiceBinding("flipt", t.flipt.AsService()).
435-
WithEnvVariable("FLIPT_URL", "http://flipt:8080").
436+
WithEnvVariable("FLIPT_URL", "https://flipt:8443").
437+
WithEnvVariable("FLIPT_CA_CERT_PATH", "/src/test/fixtures/tls/ca.crt").
436438
WithEnvVariable("FLIPT_AUTH_TOKEN", "secret").
437439
WithExec(args("go mod download")).
438440
WithExec(args("go test -v -timeout 30s ./...")).

0 commit comments

Comments
 (0)