Thanks to visit codestin.com
Credit goes to github.com

Skip to content
This repository was archived by the owner on Aug 30, 2024. It is now read-only.

Commit bc68f30

Browse files
committed
Add TrustChallenge MVP for coder-cli to inject certs
Coder-cli can pull the coderd cert using a shared secret for trust
1 parent 34d96a1 commit bc68f30

File tree

5 files changed

+142
-7
lines changed

5 files changed

+142
-7
lines changed

coder-sdk/interface.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,8 @@ type Client interface {
257257

258258
// ICEServers fetches the list of ICE servers advertised by the deployment.
259259
ICEServers(ctx context.Context) ([]webrtc.ICEServer, error)
260+
261+
// TrustEnvironment
262+
// TODO: @emyrk DO NOT USE AT THIS TIME
263+
TrustEnvironment(ctx context.Context, id string) (*TrustChallengeResponse, error)
260264
}

coder-sdk/request.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -105,23 +105,34 @@ func (c *DefaultClient) request(ctx context.Context, method, path string, in int
105105
// requestBody is a helper extending the Client.request helper, checking the response code
106106
// and decoding the response payload.
107107
func (c *DefaultClient) requestBody(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) error {
108+
_, err := c.requestBodyWithResponse(ctx, method, path, in, out, opts...)
109+
if err != nil {
110+
return err
111+
}
112+
return nil
113+
}
114+
115+
// requestBodyWithResponse is a helper extending the Client.request helper, checking the response code
116+
// and decoding the response payload. It also returns the original http.Response in case the headers or other
117+
// response properties need to be analyzed.
118+
func (c *DefaultClient) requestBodyWithResponse(ctx context.Context, method, path string, in, out interface{}, opts ...requestOption) (*http.Response, error) {
108119
resp, err := c.request(ctx, method, path, in, opts...)
109120
if err != nil {
110-
return xerrors.Errorf("Execute request: %q", err)
121+
return nil, xerrors.Errorf("Execute request: %q", err)
111122
}
112123
defer func() { _ = resp.Body.Close() }() // Best effort, likely connection dropped.
113124

114125
// Responses in the 100 are handled by the http lib, in the 200 range, we have a success.
115126
// Consider anything at or above 300 to be an error.
116127
if resp.StatusCode > 299 {
117-
return fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, NewHTTPError(resp))
128+
return nil, fmt.Errorf("unexpected status code %d: %w", resp.StatusCode, NewHTTPError(resp))
118129
}
119130

120131
// If we expect a payload, process it as json.
121132
if out != nil {
122133
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
123-
return xerrors.Errorf("decode response body: %w", err)
134+
return nil, xerrors.Errorf("decode response body: %w", err)
124135
}
125136
}
126-
return nil
137+
return resp, nil
127138
}

coder-sdk/workspace.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coder
22

33
import (
44
"context"
5+
"encoding/pem"
56
"fmt"
67
"io"
78
"net/http"
@@ -513,3 +514,34 @@ func (c *DefaultClient) SetPolicyTemplate(ctx context.Context, templateID string
513514

514515
return &resp, nil
515516
}
517+
518+
// TrustChallengeResponse returns the tls certs used in the response.
519+
type TrustChallengeResponse struct {
520+
// Certificates are the returned response certificates
521+
Certificates [][]byte `json:"-"`
522+
}
523+
524+
// TrustEnvironment is used to make a secure handshake with a coderd. This is intended to run from within a workspace.
525+
// If this fails, we are not talking to the coderd that has access to the workspace's shared secret.
526+
// TODO: @emyrk the trust challenge is not fully fleshed out, and should not be used in production at this time.
527+
// The secure handshake needs to be implemented.
528+
func (c *DefaultClient) TrustEnvironment(ctx context.Context, id string) (*TrustChallengeResponse, error) {
529+
var challenge TrustChallengeResponse
530+
//nolint: bodyclose
531+
resp, err := c.requestBodyWithResponse(ctx, http.MethodGet, "/api/private/environments/"+id+"/trust-challenge", nil, &challenge)
532+
if err != nil {
533+
return nil, err
534+
}
535+
536+
if len(resp.TLS.PeerCertificates) > 0 {
537+
for _, c := range resp.TLS.PeerCertificates {
538+
data := pem.EncodeToMemory(&pem.Block{
539+
Type: "CERTIFICATE",
540+
Bytes: c.Raw,
541+
})
542+
challenge.Certificates = append(challenge.Certificates, data)
543+
}
544+
}
545+
546+
return &challenge, nil
547+
}

internal/cmd/agent.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
package cmd
22

33
import (
4+
"context"
5+
"crypto/tls"
6+
"fmt"
7+
"net/http"
48
"net/url"
59
"os"
610
"os/signal"
11+
"path/filepath"
712
"syscall"
13+
"time"
814

915
// We use slog here since agent runs in the background and we can benefit
1016
// from structured logging.
@@ -16,6 +22,9 @@ import (
1622
"cdr.dev/coder-cli/wsnet"
1723
)
1824

25+
// coderdCertDir is where the certificates for coderd are written by the coder agent.
26+
const coderdCertDir = "/var/tmp/coder/certs"
27+
1928
func agentCmd() *cobra.Command {
2029
cmd := &cobra.Command{
2130
Use: "agent",
@@ -97,6 +106,12 @@ coder agent start --log-file=/tmp/coder-agent.log
97106
}
98107
}
99108

109+
// First inject certs
110+
err = writeCoderdCerts(ctx)
111+
if err != nil {
112+
return xerrors.Errorf("trust certs: %w", err)
113+
}
114+
100115
log.Info(ctx, "starting wsnet listener", slog.F("coder_access_url", u.String()))
101116
listener, err := wsnet.Listen(ctx, log, wsnet.ListenEndpoint(u, token), token)
102117
if err != nil {
@@ -125,3 +140,62 @@ coder agent start --log-file=/tmp/coder-agent.log
125140

126141
return cmd
127142
}
143+
144+
func writeCoderdCerts(ctx context.Context) error {
145+
// Inject certs to custom dir and concat with : with existing dir.
146+
certs, err := trustCertificate(ctx)
147+
if err != nil {
148+
return xerrors.Errorf("trust cert: %w", err)
149+
}
150+
151+
err = os.MkdirAll(coderdCertDir, 0666)
152+
if err != nil {
153+
return xerrors.Errorf("mkdir %s: %w", coderdCertDir, err)
154+
}
155+
156+
certPath := filepath.Join(coderdCertDir, "certs.pem")
157+
file, err := os.OpenFile(certPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
158+
if err != nil {
159+
return xerrors.Errorf("create file %s: %w", certPath, err)
160+
}
161+
162+
for _, cert := range certs {
163+
_, _ = fmt.Fprintln(file, string(cert))
164+
}
165+
166+
// Add our directory to the certs to trust
167+
certDir := os.Getenv("SSL_CERT_DIR")
168+
err = os.Setenv("SSL_CERT_DIR", certDir+":"+coderdCertDir)
169+
if err != nil {
170+
return xerrors.Errorf("set SSL_CERT_DIR: %w", err)
171+
}
172+
173+
return nil
174+
}
175+
176+
// trustCertificate will fetch coderd's certificate and write it to disc.
177+
// It will then extend the certs to trust to include this directory.
178+
// This only happens if coderd can answer the challenge to prove
179+
// it has the shared secret.
180+
func trustCertificate(ctx context.Context) ([][]byte, error) {
181+
conf := &tls.Config{InsecureSkipVerify: true}
182+
hc := &http.Client{
183+
Timeout: time.Second * 3,
184+
Transport: &http.Transport{
185+
TLSClientConfig: conf,
186+
},
187+
}
188+
189+
c, err := newClient(ctx, true, withHTTPClient(hc))
190+
if err != nil {
191+
return nil, xerrors.Errorf("new client: %w", err)
192+
}
193+
194+
id := os.Getenv("CODER_WORKSPACE_ID")
195+
challenge, err := c.TrustEnvironment(ctx, id)
196+
if err != nil {
197+
return nil, xerrors.Errorf("challenge failed: %w", err)
198+
}
199+
200+
return challenge.Certificates, nil
201+
}

internal/cmd/auth.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,15 @@ var errNeedLogin = clog.Fatal(
2323
const tokenEnv = "CODER_TOKEN"
2424
const urlEnv = "CODER_URL"
2525

26-
func newClient(ctx context.Context, checkVersion bool) (coder.Client, error) {
26+
type coderOpt func(opt *coder.ClientOptions)
27+
28+
func withHTTPClient(hc *http.Client) coderOpt {
29+
return func(opt *coder.ClientOptions) {
30+
opt.HTTPClient = hc
31+
}
32+
}
33+
34+
func newClient(ctx context.Context, checkVersion bool, optsF ...coderOpt) (coder.Client, error) {
2735
var (
2836
err error
2937
sessionToken = os.Getenv(tokenEnv)
@@ -47,10 +55,16 @@ func newClient(ctx context.Context, checkVersion bool) (coder.Client, error) {
4755
return nil, xerrors.Errorf("url malformed: %w try running \"coder login\" with a valid URL", err)
4856
}
4957

50-
c, err := coder.NewClient(coder.ClientOptions{
58+
clientOpts := &coder.ClientOptions{
5159
BaseURL: u,
5260
Token: sessionToken,
53-
})
61+
}
62+
63+
for _, f := range optsF {
64+
f(clientOpts)
65+
}
66+
67+
c, err := coder.NewClient(*clientOpts)
5468
if err != nil {
5569
return nil, xerrors.Errorf("failed to create new coder.Client: %w", err)
5670
}

0 commit comments

Comments
 (0)