Skip to content

Commit 6ae52ce

Browse files
authored
Merge branch 'main' into main
2 parents 15407e0 + f48a24c commit 6ae52ce

File tree

148 files changed

+5452
-2375
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+5452
-2375
lines changed

.github/actions/setup-tf/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ runs:
77
- name: Install Terraform
88
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
99
with:
10-
terraform_version: 1.11.2
10+
terraform_version: 1.11.3
1111
terraform_wrapper: false

.github/workflows/ci.yaml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1180,6 +1180,33 @@ jobs:
11801180
done
11811181
fi
11821182
1183+
- name: SBOM Generation and Attestation
1184+
if: github.ref == 'refs/heads/main'
1185+
env:
1186+
COSIGN_EXPERIMENTAL: 1
1187+
run: |
1188+
set -euxo pipefail
1189+
1190+
# Define image base and tags
1191+
IMAGE_BASE="ghcr.io/coder/coder-preview"
1192+
TAGS=("${{ steps.build-docker.outputs.tag }}" "main" "latest")
1193+
1194+
# Generate and attest SBOM for each tag
1195+
for tag in "${TAGS[@]}"; do
1196+
IMAGE="${IMAGE_BASE}:${tag}"
1197+
SBOM_FILE="coder_sbom_${tag//[:\/]/_}.spdx.json"
1198+
1199+
echo "Generating SBOM for image: ${IMAGE}"
1200+
syft "${IMAGE}" -o spdx-json > "${SBOM_FILE}"
1201+
1202+
echo "Attesting SBOM to image: ${IMAGE}"
1203+
cosign clean "${IMAGE}"
1204+
cosign attest --type spdxjson \
1205+
--predicate "${SBOM_FILE}" \
1206+
--yes \
1207+
"${IMAGE}"
1208+
done
1209+
11831210
# GitHub attestation provides SLSA provenance for the Docker images, establishing a verifiable
11841211
# record that these images were built in GitHub Actions with specific inputs and environment.
11851212
# This complements our existing cosign attestations which focus on SBOMs.

.github/workflows/release.yaml

Lines changed: 60 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,39 @@ jobs:
496496
env:
497497
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
498498

499+
- name: SBOM Generation and Attestation
500+
if: ${{ !inputs.dry_run }}
501+
env:
502+
COSIGN_EXPERIMENTAL: "1"
503+
run: |
504+
set -euxo pipefail
505+
506+
# Generate SBOM for multi-arch image with version in filename
507+
echo "Generating SBOM for multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
508+
syft "${{ steps.build_docker.outputs.multiarch_image }}" -o spdx-json > coder_${{ steps.version.outputs.version }}_sbom.spdx.json
509+
510+
# Attest SBOM to multi-arch image
511+
echo "Attesting SBOM to multi-arch image: ${{ steps.build_docker.outputs.multiarch_image }}"
512+
cosign clean "${{ steps.build_docker.outputs.multiarch_image }}"
513+
cosign attest --type spdxjson \
514+
--predicate coder_${{ steps.version.outputs.version }}_sbom.spdx.json \
515+
--yes \
516+
"${{ steps.build_docker.outputs.multiarch_image }}"
517+
518+
# If latest tag was created, also attest it
519+
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
520+
latest_tag="$(./scripts/image_tag.sh --version latest)"
521+
echo "Generating SBOM for latest image: ${latest_tag}"
522+
syft "${latest_tag}" -o spdx-json > coder_latest_sbom.spdx.json
523+
524+
echo "Attesting SBOM to latest image: ${latest_tag}"
525+
cosign clean "${latest_tag}"
526+
cosign attest --type spdxjson \
527+
--predicate coder_latest_sbom.spdx.json \
528+
--yes \
529+
"${latest_tag}"
530+
fi
531+
499532
- name: GitHub Attestation for Docker image
500533
id: attest_main
501534
if: ${{ !inputs.dry_run }}
@@ -612,16 +645,27 @@ jobs:
612645
fi
613646
declare -p publish_args
614647
648+
# Build the list of files to publish
649+
files=(
650+
./build/*_installer.exe
651+
./build/*.zip
652+
./build/*.tar.gz
653+
./build/*.tgz
654+
./build/*.apk
655+
./build/*.deb
656+
./build/*.rpm
657+
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
658+
)
659+
660+
# Only include the latest SBOM file if it was created
661+
if [[ "${{ steps.build_docker.outputs.created_latest_tag }}" == "true" ]]; then
662+
files+=(./coder_latest_sbom.spdx.json)
663+
fi
664+
615665
./scripts/release/publish.sh \
616666
"${publish_args[@]}" \
617667
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
618-
./build/*_installer.exe \
619-
./build/*.zip \
620-
./build/*.tar.gz \
621-
./build/*.tgz \
622-
./build/*.apk \
623-
./build/*.deb \
624-
./build/*.rpm
668+
"${files[@]}"
625669
env:
626670
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
627671
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
@@ -663,6 +707,15 @@ jobs:
663707
./build/*.apk
664708
./build/*.deb
665709
./build/*.rpm
710+
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
711+
retention-days: 7
712+
713+
- name: Upload latest sbom artifact to actions (if dry-run)
714+
if: inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true'
715+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
716+
with:
717+
name: latest-sbom-artifact
718+
path: ./coder_latest_sbom.spdx.json
666719
retention-days: 7
667720

668721
- name: Send repository-dispatch event

agent/agent.go

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ func (a *agent) run() (retErr error) {
907907
defer func() {
908908
cErr := aAPI.DRPCConn().Close()
909909
if cErr != nil {
910-
a.logger.Debug(a.hardCtx, "error closing drpc connection", slog.Error(err))
910+
a.logger.Debug(a.hardCtx, "error closing drpc connection", slog.Error(cErr))
911911
}
912912
}()
913913

@@ -1186,9 +1186,9 @@ func (a *agent) createOrUpdateNetwork(manifestOK, networkOK *checkpoint) func(co
11861186
network := a.network
11871187
a.closeMutex.Unlock()
11881188
if network == nil {
1189-
keySeed, err := WorkspaceKeySeed(manifest.WorkspaceID, manifest.AgentName)
1189+
keySeed, err := SSHKeySeed(manifest.OwnerName, manifest.WorkspaceName, manifest.AgentName)
11901190
if err != nil {
1191-
return xerrors.Errorf("generate seed from workspace id: %w", err)
1191+
return xerrors.Errorf("generate SSH key seed: %w", err)
11921192
}
11931193
// use the graceful context here, because creating the tailnet is not itself tied to the
11941194
// agent API.
@@ -1518,14 +1518,11 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
15181518
a.logger.Info(ctx, "connected to coordination RPC")
15191519

15201520
// This allows the Close() routine to wait for the coordinator to gracefully disconnect.
1521-
a.closeMutex.Lock()
1522-
if a.isClosed() {
1523-
return nil
1521+
disconnected := a.setCoordDisconnected()
1522+
if disconnected == nil {
1523+
return nil // already closed by something else
15241524
}
1525-
disconnected := make(chan struct{})
1526-
a.coordDisconnected = disconnected
15271525
defer close(disconnected)
1528-
a.closeMutex.Unlock()
15291526

15301527
ctrl := tailnet.NewAgentCoordinationController(a.logger, network)
15311528
coordination := ctrl.New(coordinate)
@@ -1547,6 +1544,17 @@ func (a *agent) runCoordinator(ctx context.Context, tClient tailnetproto.DRPCTai
15471544
return <-errCh
15481545
}
15491546

1547+
func (a *agent) setCoordDisconnected() chan struct{} {
1548+
a.closeMutex.Lock()
1549+
defer a.closeMutex.Unlock()
1550+
if a.isClosed() {
1551+
return nil
1552+
}
1553+
disconnected := make(chan struct{})
1554+
a.coordDisconnected = disconnected
1555+
return disconnected
1556+
}
1557+
15501558
// runDERPMapSubscriber runs a coordinator and returns if a reconnect should occur.
15511559
func (a *agent) runDERPMapSubscriber(ctx context.Context, tClient tailnetproto.DRPCTailnetClient24, network *tailnet.Conn) error {
15521560
defer a.logger.Debug(ctx, "disconnected from derp map RPC")
@@ -2068,12 +2076,31 @@ func PrometheusMetricsHandler(prometheusRegistry *prometheus.Registry, logger sl
20682076
})
20692077
}
20702078

2071-
// WorkspaceKeySeed converts a WorkspaceID UUID and agent name to an int64 hash.
2079+
// SSHKeySeed converts an owner userName, workspaceName and agentName to an int64 hash.
20722080
// This uses the FNV-1a hash algorithm which provides decent distribution and collision
20732081
// resistance for string inputs.
2074-
func WorkspaceKeySeed(workspaceID uuid.UUID, agentName string) (int64, error) {
2082+
//
2083+
// Why owner username, workspace name, and agent name? These are the components that are used in hostnames for the
2084+
// workspace over SSH, and so we want the workspace to have a stable key with respect to these. We don't use the
2085+
// respective UUIDs. The workspace UUID would be different if you delete and recreate a workspace with the same name.
2086+
// The agent UUID is regenerated on each build. Since Coder's Tailnet networking is handling the authentication, we
2087+
// should not be showing users warnings about host SSH keys.
2088+
func SSHKeySeed(userName, workspaceName, agentName string) (int64, error) {
20752089
h := fnv.New64a()
2076-
_, err := h.Write(workspaceID[:])
2090+
_, err := h.Write([]byte(userName))
2091+
if err != nil {
2092+
return 42, err
2093+
}
2094+
// null separators between strings so that (dog, foodstuff) is distinct from (dogfood, stuff)
2095+
_, err = h.Write([]byte{0})
2096+
if err != nil {
2097+
return 42, err
2098+
}
2099+
_, err = h.Write([]byte(workspaceName))
2100+
if err != nil {
2101+
return 42, err
2102+
}
2103+
_, err = h.Write([]byte{0})
20772104
if err != nil {
20782105
return 42, err
20792106
}

agent/agentssh/agentssh.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,8 +1060,10 @@ func (s *Server) Close() error {
10601060
// Guard against multiple calls to Close and
10611061
// accepting new connections during close.
10621062
if s.closing != nil {
1063+
closing := s.closing
10631064
s.mu.Unlock()
1064-
return xerrors.New("server is closing")
1065+
<-closing
1066+
return xerrors.New("server is closed")
10651067
}
10661068
s.closing = make(chan struct{})
10671069

agent/agentssh/agentssh_test.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ func TestNewServer_CloseActiveConnections(t *testing.T) {
153153
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
154154
s, err := agentssh.NewServer(ctx, logger, prometheus.NewRegistry(), afero.NewMemMapFs(), agentexec.DefaultExecer, nil)
155155
require.NoError(t, err)
156-
defer s.Close()
156+
t.Cleanup(func() {
157+
_ = s.Close()
158+
})
157159
err = s.UpdateHostSigner(42)
158160
assert.NoError(t, err)
159161

@@ -190,10 +192,17 @@ func TestNewServer_CloseActiveConnections(t *testing.T) {
190192
}
191193
// The 60 seconds here is intended to be longer than the
192194
// test. The shutdown should propagate.
193-
err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; sleep 60'")
195+
if runtime.GOOS == "windows" {
196+
// Best effort to at least partially test this in Windows.
197+
err = sess.Start("echo start\"ed\" && sleep 60")
198+
} else {
199+
err = sess.Start("/bin/bash -c 'trap \"sleep 60\" SIGTERM; echo start\"ed\"; sleep 60'")
200+
}
194201
assert.NoError(t, err)
195202

203+
pty.ExpectMatchContext(ctx, "started")
196204
close(ch)
205+
197206
err = sess.Wait()
198207
assert.Error(t, err)
199208
}(waitConns[i])

agent/agentssh/exec_windows.go

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

33
import (
44
"context"
5-
"os"
65
"os/exec"
76
"syscall"
87

@@ -15,7 +14,12 @@ func cmdSysProcAttr() *syscall.SysProcAttr {
1514

1615
func cmdCancel(ctx context.Context, logger slog.Logger, cmd *exec.Cmd) func() error {
1716
return func() error {
18-
logger.Debug(ctx, "cmdCancel: sending interrupt to process", slog.F("pid", cmd.Process.Pid))
19-
return cmd.Process.Signal(os.Interrupt)
17+
logger.Debug(ctx, "cmdCancel: killing process", slog.F("pid", cmd.Process.Pid))
18+
// Windows doesn't support sending signals to process groups, so we
19+
// have to kill the process directly. In the future, we may want to
20+
// implement a more sophisticated solution for process groups on
21+
// Windows, but for now, this is a simple way to ensure that the
22+
// process is terminated when the context is cancelled.
23+
return cmd.Process.Kill()
2024
}
2125
}

cli/configssh.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ const (
4545
// sshConfigOptions represents options that can be stored and read
4646
// from the coder config in ~/.ssh/coder.
4747
type sshConfigOptions struct {
48-
waitEnum string
48+
waitEnum string
49+
// Deprecated: moving away from prefix to hostnameSuffix
4950
userHostPrefix string
51+
hostnameSuffix string
5052
sshOptions []string
5153
disableAutostart bool
5254
header []string
@@ -97,7 +99,11 @@ func (o sshConfigOptions) equal(other sshConfigOptions) bool {
9799
if !slicesSortedEqual(o.header, other.header) {
98100
return false
99101
}
100-
return o.waitEnum == other.waitEnum && o.userHostPrefix == other.userHostPrefix && o.disableAutostart == other.disableAutostart && o.headerCommand == other.headerCommand
102+
return o.waitEnum == other.waitEnum &&
103+
o.userHostPrefix == other.userHostPrefix &&
104+
o.disableAutostart == other.disableAutostart &&
105+
o.headerCommand == other.headerCommand &&
106+
o.hostnameSuffix == other.hostnameSuffix
101107
}
102108

103109
// slicesSortedEqual compares two slices without side-effects or regard to order.
@@ -119,6 +125,9 @@ func (o sshConfigOptions) asList() (list []string) {
119125
if o.userHostPrefix != "" {
120126
list = append(list, fmt.Sprintf("ssh-host-prefix: %s", o.userHostPrefix))
121127
}
128+
if o.hostnameSuffix != "" {
129+
list = append(list, fmt.Sprintf("hostname-suffix: %s", o.hostnameSuffix))
130+
}
122131
if o.disableAutostart {
123132
list = append(list, fmt.Sprintf("disable-autostart: %v", o.disableAutostart))
124133
}
@@ -314,6 +323,10 @@ func (r *RootCmd) configSSH() *serpent.Command {
314323
// Override with user flag.
315324
coderdConfig.HostnamePrefix = sshConfigOpts.userHostPrefix
316325
}
326+
if sshConfigOpts.hostnameSuffix != "" {
327+
// Override with user flag.
328+
coderdConfig.HostnameSuffix = sshConfigOpts.hostnameSuffix
329+
}
317330

318331
// Write agent configuration.
319332
defaultOptions := []string{
@@ -518,6 +531,12 @@ func (r *RootCmd) configSSH() *serpent.Command {
518531
Description: "Override the default host prefix.",
519532
Value: serpent.StringOf(&sshConfigOpts.userHostPrefix),
520533
},
534+
{
535+
Flag: "hostname-suffix",
536+
Env: "CODER_CONFIGSSH_HOSTNAME_SUFFIX",
537+
Description: "Override the default hostname suffix.",
538+
Value: serpent.StringOf(&sshConfigOpts.hostnameSuffix),
539+
},
521540
{
522541
Flag: "wait",
523542
Env: "CODER_CONFIGSSH_WAIT", // Not to be mixed with CODER_SSH_WAIT.
@@ -568,6 +587,9 @@ func sshConfigWriteSectionHeader(w io.Writer, addNewline bool, o sshConfigOption
568587
if o.userHostPrefix != "" {
569588
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "ssh-host-prefix", o.userHostPrefix)
570589
}
590+
if o.hostnameSuffix != "" {
591+
_, _ = fmt.Fprintf(&ow, "# :%s=%s\n", "hostname-suffix", o.hostnameSuffix)
592+
}
571593
if o.disableAutostart {
572594
_, _ = fmt.Fprintf(&ow, "# :%s=%v\n", "disable-autostart", o.disableAutostart)
573595
}
@@ -607,6 +629,8 @@ func sshConfigParseLastOptions(r io.Reader) (o sshConfigOptions) {
607629
o.waitEnum = parts[1]
608630
case "ssh-host-prefix":
609631
o.userHostPrefix = parts[1]
632+
case "hostname-suffix":
633+
o.hostnameSuffix = parts[1]
610634
case "ssh-option":
611635
o.sshOptions = append(o.sshOptions, parts[1])
612636
case "disable-autostart":

cli/configssh_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -432,6 +432,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
432432
"# Last config-ssh options:",
433433
"# :wait=yes",
434434
"# :ssh-host-prefix=coder-test.",
435+
"# :hostname-suffix=coder-suffix",
435436
"# :header=X-Test-Header=foo",
436437
"# :header=X-Test-Header2=bar",
437438
"# :header-command=printf h1=v1 h2=\"v2\" h3='v3'",
@@ -447,6 +448,7 @@ func TestConfigSSH_FileWriteAndOptionsFlow(t *testing.T) {
447448
"--yes",
448449
"--wait=yes",
449450
"--ssh-host-prefix", "coder-test.",
451+
"--hostname-suffix", "coder-suffix",
450452
"--header", "X-Test-Header=foo",
451453
"--header", "X-Test-Header2=bar",
452454
"--header-command", "printf h1=v1 h2=\"v2\" h3='v3'",

0 commit comments

Comments
 (0)