detached 4fd0c2f

fix(cli): add global --insecure for remote TLS verification

commit
4fd0c2f58bcebc2fa772f438b34d74b90fdf4e27
author
brice · · 2026-02-06 19:38 UTC
committer
brice · · 2026-02-06 19:38 UTC
parent
800ace0
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
5 files · +74 -9
M docs/cli/index.md +6 -0
@@ -109,6 +109,7 @@ gordon remotes use prod
109109 | `-c, --config` | Path to configuration file |
110110 | `--remote` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |
111111 | `--token` | Authentication token for remote |
112+| `--insecure` | Skip TLS certificate verification for remote HTTPS endpoints |
112113
113114 ### Remote Targeting
114115
@@ -118,10 +119,15 @@ when you want to bypass your saved configuration.
118119
119120 **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API.
120121
122+Use `--insecure` when the remote endpoint uses a self-signed or otherwise untrusted TLS certificate.
123+
121124 ```bash
122125 # Using flags (use the gordon_domain from remote Gordon config)
123126 gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
124127
128+# Against self-signed/private CA endpoint
129+gordon --remote https://gordon.mydomain.com --token $TOKEN --insecure status
130+
125131 # Using environment variables
126132 export GORDON_REMOTE=https://gordon.mydomain.com
127133 export GORDON_TOKEN=$TOKEN
@@ -109,6 +109,7 @@ gordon remotes use prod
109 | `-c, --config` | Path to configuration file |109 | `-c, --config` | Path to configuration file |
110 | `--remote` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |110 | `--remote` | Remote Gordon URL (e.g., `https://gordon.mydomain.com`) |
111 | `--token` | Authentication token for remote |111 | `--token` | Authentication token for remote |
112+| `--insecure` | Skip TLS certificate verification for remote HTTPS endpoints |
112 113
113 ### Remote Targeting114 ### Remote Targeting
114 115
@@ -118,10 +119,15 @@ when you want to bypass your saved configuration.
118 119
119 **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API.120 **Important:** The remote URL must be the `gordon_domain` configured on the remote Gordon instance. This is the domain that serves both the container registry and the Admin API.
120 121
122+Use `--insecure` when the remote endpoint uses a self-signed or otherwise untrusted TLS certificate.
123+
121 ```bash124 ```bash
122 # Using flags (use the gordon_domain from remote Gordon config)125 # Using flags (use the gordon_domain from remote Gordon config)
123 gordon routes list --remote https://gordon.mydomain.com --token $TOKEN126 gordon routes list --remote https://gordon.mydomain.com --token $TOKEN
124 127
128+# Against self-signed/private CA endpoint
129+gordon --remote https://gordon.mydomain.com --token $TOKEN --insecure status
130+
125 # Using environment variables131 # Using environment variables
126 export GORDON_REMOTE=https://gordon.mydomain.com132 export GORDON_REMOTE=https://gordon.mydomain.com
127 export GORDON_TOKEN=$TOKEN133 export GORDON_TOKEN=$TOKEN
M internal/adapters/in/cli/auth.go +3 -3
@@ -297,7 +297,7 @@ func runAuthLogin(remoteName, username, password string) error {
297297 }
298298
299299 // Create remote client
300- client := remote.NewClient(remoteConfig.URL)
300+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions("")...)
301301
302302 // Authenticate
303303 ctx := context.Background()
@@ -332,7 +332,7 @@ func runAuthLoginWithToken(remoteName, token string) error {
332332 return err
333333 }
334334
335- client := remote.NewClient(remoteConfig.URL, remote.WithToken(token))
335+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions(token)...)
336336 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
337337 defer cancel()
338338 verified := true
@@ -758,7 +758,7 @@ func runAuthStatus(remoteNameArg string) error {
758758 }
759759
760760 // Create client
761- client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token))
761+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions(remoteConfig.Token)...)
762762
763763 // Verify auth
764764 ctx := context.Background()
@@ -297,7 +297,7 @@ func runAuthLogin(remoteName, username, password string) error {
297 }297 }
298 298
299 // Create remote client299 // Create remote client
300- client := remote.NewClient(remoteConfig.URL)300+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions("")...)
301 301
302 // Authenticate302 // Authenticate
303 ctx := context.Background()303 ctx := context.Background()
@@ -332,7 +332,7 @@ func runAuthLoginWithToken(remoteName, token string) error {
332 return err332 return err
333 }333 }
334 334
335- client := remote.NewClient(remoteConfig.URL, remote.WithToken(token))335+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions(token)...)
336 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)336 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
337 defer cancel()337 defer cancel()
338 verified := true338 verified := true
@@ -758,7 +758,7 @@ func runAuthStatus(remoteNameArg string) error {
758 }758 }
759 759
760 // Create client760 // Create client
761- client := remote.NewClient(remoteConfig.URL, remote.WithToken(remoteConfig.Token))761+ client := remote.NewClient(remoteConfig.URL, remoteClientOptions(remoteConfig.Token)...)
762 762
763 // Verify auth763 // Verify auth
764 ctx := context.Background()764 ctx := context.Background()
M internal/adapters/in/cli/remote/client.go +34 -3
@@ -4,6 +4,7 @@ package remote
44 import (
55 "bytes"
66 "context"
7+ "crypto/tls"
78 "encoding/json"
89 "errors"
910 "fmt"
@@ -19,9 +20,10 @@ import (
1920
2021 // Client is an HTTP client for the Gordon admin API.
2122 type Client struct {
22- baseURL string
23- token string
24- httpClient *http.Client
23+ baseURL string
24+ token string
25+ httpClient *http.Client
26+ insecureTLS bool
2527 }
2628
2729 var (
@@ -48,6 +50,8 @@ func NewClient(baseURL string, opts ...ClientOption) *Client {
4850 opt(c)
4951 }
5052
53+ c.applyTLSConfig()
54+
5155 return c
5256 }
5357
@@ -72,6 +76,33 @@ func WithTimeout(timeout time.Duration) ClientOption {
7276 }
7377 }
7478
79+// WithInsecureTLS disables TLS certificate verification for remote admin API requests.
80+func WithInsecureTLS(insecure bool) ClientOption {
81+ return func(c *Client) {
82+ c.insecureTLS = insecure
83+ }
84+}
85+
86+func (c *Client) applyTLSConfig() {
87+ if !c.insecureTLS {
88+ return
89+ }
90+
91+ var transport *http.Transport
92+ if existing, ok := c.httpClient.Transport.(*http.Transport); ok {
93+ transport = existing.Clone()
94+ } else {
95+ transport = http.DefaultTransport.(*http.Transport).Clone()
96+ }
97+
98+ if transport.TLSClientConfig == nil {
99+ transport.TLSClientConfig = &tls.Config{}
100+ }
101+ //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
102+ transport.TLSClientConfig.InsecureSkipVerify = true
103+ c.httpClient.Transport = transport
104+}
105+
75106 // request performs an HTTP request to the admin API.
76107 func (c *Client) request(ctx context.Context, method, path string, body any) (*http.Response, error) {
77108 url := c.baseURL + "/admin" + path
@@ -4,6 +4,7 @@ package remote
4 import (4 import (
5 "bytes"5 "bytes"
6 "context"6 "context"
7+ "crypto/tls"
7 "encoding/json"8 "encoding/json"
8 "errors"9 "errors"
9 "fmt"10 "fmt"
@@ -19,9 +20,10 @@ import (
19 20
20 // Client is an HTTP client for the Gordon admin API.21 // Client is an HTTP client for the Gordon admin API.
21 type Client struct {22 type Client struct {
22- baseURL string23+ baseURL string
23- token string24+ token string
24- httpClient *http.Client25+ httpClient *http.Client
26+ insecureTLS bool
25 }27 }
26 28
27 var (29 var (
@@ -48,6 +50,8 @@ func NewClient(baseURL string, opts ...ClientOption) *Client {
48 opt(c)50 opt(c)
49 }51 }
50 52
53+ c.applyTLSConfig()
54+
51 return c55 return c
52 }56 }
53 57
@@ -72,6 +76,33 @@ func WithTimeout(timeout time.Duration) ClientOption {
72 }76 }
73 }77 }
74 78
79+// WithInsecureTLS disables TLS certificate verification for remote admin API requests.
80+func WithInsecureTLS(insecure bool) ClientOption {
81+ return func(c *Client) {
82+ c.insecureTLS = insecure
83+ }
84+}
85+
86+func (c *Client) applyTLSConfig() {
87+ if !c.insecureTLS {
88+ return
89+ }
90+
91+ var transport *http.Transport
92+ if existing, ok := c.httpClient.Transport.(*http.Transport); ok {
93+ transport = existing.Clone()
94+ } else {
95+ transport = http.DefaultTransport.(*http.Transport).Clone()
96+ }
97+
98+ if transport.TLSClientConfig == nil {
99+ transport.TLSClientConfig = &tls.Config{}
100+ }
101+ //nolint:gosec // Explicit CLI opt-in via --insecure for self-signed/private cert deployments.
102+ transport.TLSClientConfig.InsecureSkipVerify = true
103+ c.httpClient.Transport = transport
104+}
105+
75 // request performs an HTTP request to the admin API.106 // request performs an HTTP request to the admin API.
76 func (c *Client) request(ctx context.Context, method, path string, body any) (*http.Response, error) {107 func (c *Client) request(ctx context.Context, method, path string, body any) (*http.Response, error) {
77 url := c.baseURL + "/admin" + path108 url := c.baseURL + "/admin" + path
M internal/adapters/in/cli/remote/client_test.go +15 -0
@@ -93,3 +93,18 @@ func TestClientDeployReturnsErrorAfterRetryExhaustion(t *testing.T) {
9393 assert.Contains(t, err.Error(), "500 Internal Server Error: failed to deploy container")
9494 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))
9595 }
96+
97+func TestClientWithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
98+ srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99+ require.Equal(t, "/admin/status", r.URL.Path)
100+ w.Header().Set("Content-Type", "application/json")
101+ _, _ = w.Write([]byte(`{"routes":0,"registry_domain":"","registry_port":0,"server_port":0,"auto_route":false,"network_isolation":false,"container_status":{}}`))
102+ }))
103+ defer srv.Close()
104+
105+ _, err := NewClient(srv.URL).GetStatus(context.Background())
106+ require.Error(t, err)
107+
108+ _, err = NewClient(srv.URL, WithInsecureTLS(true)).GetStatus(context.Background())
109+ require.NoError(t, err)
110+}
@@ -93,3 +93,18 @@ func TestClientDeployReturnsErrorAfterRetryExhaustion(t *testing.T) {
93 assert.Contains(t, err.Error(), "500 Internal Server Error: failed to deploy container")93 assert.Contains(t, err.Error(), "500 Internal Server Error: failed to deploy container")
94 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))94 assert.EqualValues(t, retryMaxAttempts, atomic.LoadInt32(&attempts))
95 }95 }
96+
97+func TestClientWithInsecureTLS_AllowsSelfSignedCertificate(t *testing.T) {
98+ srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
99+ require.Equal(t, "/admin/status", r.URL.Path)
100+ w.Header().Set("Content-Type", "application/json")
101+ _, _ = w.Write([]byte(`{"routes":0,"registry_domain":"","registry_port":0,"server_port":0,"auto_route":false,"network_isolation":false,"container_status":{}}`))
102+ }))
103+ defer srv.Close()
104+
105+ _, err := NewClient(srv.URL).GetStatus(context.Background())
106+ require.Error(t, err)
107+
108+ _, err = NewClient(srv.URL, WithInsecureTLS(true)).GetStatus(context.Background())
109+ require.NoError(t, err)
110+}
M internal/adapters/in/cli/root.go +16 -3
@@ -22,8 +22,9 @@ var (
2222 BuildDate = "unknown"
2323
2424 // Global flags for remote targeting
25- remoteFlag string
26- tokenFlag string
25+ remoteFlag string
26+ tokenFlag string
27+ insecureTLSFlag bool
2728 )
2829
2930 // Command group IDs
@@ -60,6 +61,7 @@ Commands are organized by where they run:
6061 // Add persistent flags for remote targeting
6162 rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
6263 rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")
64+ rootCmd.PersistentFlags().BoolVar(&insecureTLSFlag, "insecure", false, "Skip TLS certificate verification for remote HTTPS endpoints")
6365
6466 // Server-only commands (must run on the Gordon host)
6567 serveCmd := newServeCmd()
@@ -139,10 +141,21 @@ func GetRemoteClient() (*remote.Client, bool) {
139141 return nil, false
140142 }
141143
142- client := remote.NewClient(url, remote.WithToken(token))
144+ client := remote.NewClient(url, remoteClientOptions(token)...)
143145 return client, true
144146 }
145147
148+func remoteClientOptions(token string) []remote.ClientOption {
149+ opts := make([]remote.ClientOption, 0, 2)
150+ if token != "" {
151+ opts = append(opts, remote.WithToken(token))
152+ }
153+ if insecureTLSFlag {
154+ opts = append(opts, remote.WithInsecureTLS(true))
155+ }
156+ return opts
157+}
158+
146159 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.
147160 func IsRemoteMode() bool {
148161 _, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)
@@ -22,8 +22,9 @@ var (
22 BuildDate = "unknown"22 BuildDate = "unknown"
23 23
24 // Global flags for remote targeting24 // Global flags for remote targeting
25- remoteFlag string25+ remoteFlag string
26- tokenFlag string26+ tokenFlag string
27+ insecureTLSFlag bool
27 )28 )
28 29
29 // Command group IDs30 // Command group IDs
@@ -60,6 +61,7 @@ Commands are organized by where they run:
60 // Add persistent flags for remote targeting61 // Add persistent flags for remote targeting
61 rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")62 rootCmd.PersistentFlags().StringVar(&remoteFlag, "remote", "", "Remote Gordon URL (e.g., https://gordon.mydomain.com)")
62 rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")63 rootCmd.PersistentFlags().StringVar(&tokenFlag, "token", "", "Authentication token for remote")
64+ rootCmd.PersistentFlags().BoolVar(&insecureTLSFlag, "insecure", false, "Skip TLS certificate verification for remote HTTPS endpoints")
63 65
64 // Server-only commands (must run on the Gordon host)66 // Server-only commands (must run on the Gordon host)
65 serveCmd := newServeCmd()67 serveCmd := newServeCmd()
@@ -139,10 +141,21 @@ func GetRemoteClient() (*remote.Client, bool) {
139 return nil, false141 return nil, false
140 }142 }
141 143
142- client := remote.NewClient(url, remote.WithToken(token))144+ client := remote.NewClient(url, remoteClientOptions(token)...)
143 return client, true145 return client, true
144 }146 }
145 147
148+func remoteClientOptions(token string) []remote.ClientOption {
149+ opts := make([]remote.ClientOption, 0, 2)
150+ if token != "" {
151+ opts = append(opts, remote.WithToken(token))
152+ }
153+ if insecureTLSFlag {
154+ opts = append(opts, remote.WithInsecureTLS(true))
155+ }
156+ return opts
157+}
158+
146 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.159 // IsRemoteMode returns true if CLI is targeting a remote Gordon instance.
147 func IsRemoteMode() bool {160 func IsRemoteMode() bool {
148 _, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)161 _, _, isRemote := remote.ResolveRemote(remoteFlag, tokenFlag)

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close