detached 9f8c7d2

fix(dgram): redact and clear bootstrap keys promptly

commit
9f8c7d27f7cdb324c751e61442f1de18e39d2e65
author
brice · · 2026-07-23 20:29 UTC
parent
93af417
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +44 -20
M internal/adapters/dgram/ssh_dialer.go +32 -15
@@ -158,6 +158,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) {
158158 cleanup = false
159159 return nil, udpUnavailable("read bootstrap readiness", err, &stderr)
160160 }
161+ defer pdgram.Erase(ready.key)
161162 if err := waitBootstrapContext(bootstrapCtx, proc); err != nil {
162163 waited = true
163164 cleanup = false
@@ -182,6 +183,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) {
182183 return listenUDPPacket(ctx)
183184 },
184185 }, WithRuntimeObserver(d.RuntimeObserver))
186+ pdgram.Erase(ready.key)
185187 if err != nil {
186188 _ = pc.Close()
187189 return nil, udpUnavailable("create UDP transport", err, &stderr)
@@ -251,22 +253,37 @@ func readUDPReady(r io.Reader) (udpReady, error) {
251253 if err != nil && line == "" {
252254 return udpReady{}, err
253255 }
254- fields := strings.Fields(line)
255- if len(fields) != 3 || fields[0] != "VEV-UDP" {
256- return udpReady{}, fmt.Errorf("malformed readiness line %q", strings.TrimSpace(line))
257- }
258- port, err := strconv.Atoi(fields[1])
259- if err != nil || port < 1 || port > 65535 {
260- return udpReady{}, fmt.Errorf("invalid UDP port %q", fields[1])
261- }
262- key, err := base64.StdEncoding.DecodeString(fields[2])
263- if err != nil {
264- return udpReady{}, fmt.Errorf("invalid bootstrap key: %w", err)
265- }
266- if len(key) != pdgram.KeySize {
267- return udpReady{}, fmt.Errorf("invalid bootstrap key length %d", len(key))
256+
257+ ready := udpReady{key: make([]byte, pdgram.KeySize)}
258+ var parseErr error
259+ pdgram.SecretDo(func() {
260+ fields := strings.Fields(line)
261+ if len(fields) != 3 || fields[0] != "VEV-UDP" {
262+ parseErr = fmt.Errorf("malformed UDP readiness line")
263+ return
264+ }
265+ ready.port, parseErr = strconv.Atoi(fields[1])
266+ if parseErr != nil || ready.port < 1 || ready.port > 65535 {
267+ parseErr = fmt.Errorf("invalid UDP port %q", fields[1])
268+ return
269+ }
270+ key, decodeErr := base64.StdEncoding.DecodeString(fields[2])
271+ defer pdgram.Erase(key)
272+ if decodeErr != nil {
273+ parseErr = fmt.Errorf("invalid bootstrap key: %w", decodeErr)
274+ return
275+ }
276+ if len(key) != pdgram.KeySize {
277+ parseErr = fmt.Errorf("invalid bootstrap key length %d", len(key))
278+ return
279+ }
280+ copy(ready.key, key)
281+ })
282+ if parseErr != nil {
283+ pdgram.Erase(ready.key)
284+ return udpReady{}, parseErr
268285 }
269- return udpReady{port: port, key: key}, nil
286+ return ready, nil
270287 }
271288
272289 func udpUnavailable(action string, err error, stderr fmt.Stringer) error {
@@ -158,6 +158,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) {
158 cleanup = false158 cleanup = false
159 return nil, udpUnavailable("read bootstrap readiness", err, &stderr)159 return nil, udpUnavailable("read bootstrap readiness", err, &stderr)
160 }160 }
161+ defer pdgram.Erase(ready.key)
161 if err := waitBootstrapContext(bootstrapCtx, proc); err != nil {162 if err := waitBootstrapContext(bootstrapCtx, proc); err != nil {
162 waited = true163 waited = true
163 cleanup = false164 cleanup = false
@@ -182,6 +183,7 @@ func (d RemoteDialer) Dial(ctx context.Context) (ports.Transport, error) {
182 return listenUDPPacket(ctx)183 return listenUDPPacket(ctx)
183 },184 },
184 }, WithRuntimeObserver(d.RuntimeObserver))185 }, WithRuntimeObserver(d.RuntimeObserver))
186+ pdgram.Erase(ready.key)
185 if err != nil {187 if err != nil {
186 _ = pc.Close()188 _ = pc.Close()
187 return nil, udpUnavailable("create UDP transport", err, &stderr)189 return nil, udpUnavailable("create UDP transport", err, &stderr)
@@ -251,22 +253,37 @@ func readUDPReady(r io.Reader) (udpReady, error) {
251 if err != nil && line == "" {253 if err != nil && line == "" {
252 return udpReady{}, err254 return udpReady{}, err
253 }255 }
254- fields := strings.Fields(line)256+
255- if len(fields) != 3 || fields[0] != "VEV-UDP" {257+ ready := udpReady{key: make([]byte, pdgram.KeySize)}
256- return udpReady{}, fmt.Errorf("malformed readiness line %q", strings.TrimSpace(line))258+ var parseErr error
257- }259+ pdgram.SecretDo(func() {
258- port, err := strconv.Atoi(fields[1])260+ fields := strings.Fields(line)
259- if err != nil || port < 1 || port > 65535 {261+ if len(fields) != 3 || fields[0] != "VEV-UDP" {
260- return udpReady{}, fmt.Errorf("invalid UDP port %q", fields[1])262+ parseErr = fmt.Errorf("malformed UDP readiness line")
261- }263+ return
262- key, err := base64.StdEncoding.DecodeString(fields[2])264+ }
263- if err != nil {265+ ready.port, parseErr = strconv.Atoi(fields[1])
264- return udpReady{}, fmt.Errorf("invalid bootstrap key: %w", err)266+ if parseErr != nil || ready.port < 1 || ready.port > 65535 {
265- }267+ parseErr = fmt.Errorf("invalid UDP port %q", fields[1])
266- if len(key) != pdgram.KeySize {268+ return
267- return udpReady{}, fmt.Errorf("invalid bootstrap key length %d", len(key))269+ }
270+ key, decodeErr := base64.StdEncoding.DecodeString(fields[2])
271+ defer pdgram.Erase(key)
272+ if decodeErr != nil {
273+ parseErr = fmt.Errorf("invalid bootstrap key: %w", decodeErr)
274+ return
275+ }
276+ if len(key) != pdgram.KeySize {
277+ parseErr = fmt.Errorf("invalid bootstrap key length %d", len(key))
278+ return
279+ }
280+ copy(ready.key, key)
281+ })
282+ if parseErr != nil {
283+ pdgram.Erase(ready.key)
284+ return udpReady{}, parseErr
268 }285 }
269- return udpReady{port: port, key: key}, nil286+ return ready, nil
270 }287 }
271 288
272 func udpUnavailable(action string, err error, stderr fmt.Stringer) error {289 func udpUnavailable(action string, err error, stderr fmt.Stringer) error {
M internal/adapters/dgram/ssh_dialer_test.go +12 -5
@@ -60,7 +60,7 @@ func TestRemoteDialerUDPBootstrapFailures(t *testing.T) {
6060 wantStdoutClosed bool
6161 }{
6262 {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true},
63- {name: "malformed readiness", stdout: "hello\n", want: "malformed readiness line", killed: true, waited: true, wantStdoutClosed: true},
63+ {name: "malformed readiness", stdout: "hello\n", want: "malformed UDP readiness line", killed: true, waited: true, wantStdoutClosed: true},
6464 {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true},
6565 {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true},
6666 }
@@ -180,18 +180,25 @@ func TestReadUDPReady(t *testing.T) {
180180 name, line string
181181 wantPort int
182182 wantErr string
183+ exactErr bool
183184 }{
184185 {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234},
185- {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed readiness"},
186- {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: "invalid UDP port"},
186+ {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed UDP readiness line", exactErr: true},
187+ {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: `invalid UDP port "99999"`, exactErr: true},
187188 {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"},
188189 }
189190 for _, tt := range tests {
190191 t.Run(tt.name, func(t *testing.T) {
191192 got, err := readUDPReady(strings.NewReader(tt.line))
192193 if tt.wantErr != "" {
193- if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
194- t.Fatalf("err=%v, want %q", err, tt.wantErr)
194+ if err == nil {
195+ t.Fatalf("err=nil, want %q", tt.wantErr)
196+ }
197+ if tt.exactErr && err.Error() != tt.wantErr {
198+ t.Fatalf("err=%q, want exactly %q", err, tt.wantErr)
199+ }
200+ if !tt.exactErr && !strings.Contains(err.Error(), tt.wantErr) {
201+ t.Fatalf("err=%v, want containing %q", err, tt.wantErr)
195202 }
196203 return
197204 }
@@ -60,7 +60,7 @@ func TestRemoteDialerUDPBootstrapFailures(t *testing.T) {
60 wantStdoutClosed bool60 wantStdoutClosed bool
61 }{61 }{
62 {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true},62 {name: "start failure", start: errors.New("ssh missing"), want: "remote UDP transport unavailable: start bootstrap: ssh missing", wantStdoutClosed: true},
63- {name: "malformed readiness", stdout: "hello\n", want: "malformed readiness line", killed: true, waited: true, wantStdoutClosed: true},63+ {name: "malformed readiness", stdout: "hello\n", want: "malformed UDP readiness line", killed: true, waited: true, wantStdoutClosed: true},
64 {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true},64 {name: "bootstrap wait failure", stdout: "VEV-UDP 4444 " + key + "\n", want: "wait bootstrap: exit status 255", waited: true, wantStdoutClosed: true},
65 {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true},65 {name: "packet listen failure", stdout: "VEV-UDP 4444 " + key + "\n", listen: errors.New("bind denied"), want: "listen UDP: bind denied", waited: true, wantStdoutClosed: true},
66 }66 }
@@ -180,18 +180,25 @@ func TestReadUDPReady(t *testing.T) {
180 name, line string180 name, line string
181 wantPort int181 wantPort int
182 wantErr string182 wantErr string
183+ exactErr bool
183 }{184 }{
184 {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234},185 {name: "valid", line: "VEV-UDP 1234 " + key + "\n", wantPort: 1234},
185- {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed readiness"},186+ {name: "bad prefix", line: "NOPE 1234 " + key + "\n", wantErr: "malformed UDP readiness line", exactErr: true},
186- {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: "invalid UDP port"},187+ {name: "bad port", line: "VEV-UDP 99999 " + key + "\n", wantErr: `invalid UDP port "99999"`, exactErr: true},
187 {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"},188 {name: "bad key", line: "VEV-UDP 1234 !!!\n", wantErr: "invalid bootstrap key"},
188 }189 }
189 for _, tt := range tests {190 for _, tt := range tests {
190 t.Run(tt.name, func(t *testing.T) {191 t.Run(tt.name, func(t *testing.T) {
191 got, err := readUDPReady(strings.NewReader(tt.line))192 got, err := readUDPReady(strings.NewReader(tt.line))
192 if tt.wantErr != "" {193 if tt.wantErr != "" {
193- if err == nil || !strings.Contains(err.Error(), tt.wantErr) {194+ if err == nil {
194- t.Fatalf("err=%v, want %q", err, tt.wantErr)195+ t.Fatalf("err=nil, want %q", tt.wantErr)
196+ }
197+ if tt.exactErr && err.Error() != tt.wantErr {
198+ t.Fatalf("err=%q, want exactly %q", err, tt.wantErr)
199+ }
200+ if !tt.exactErr && !strings.Contains(err.Error(), tt.wantErr) {
201+ t.Fatalf("err=%v, want containing %q", err, tt.wantErr)
195 }202 }
196 return203 return
197 }204 }

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 12

Tags 11

↑↓ moveenter openesc close