Skip to content

Add support for SSH known hosts in git sources #729

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion source_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ type GitAuth struct {
// Note: This should not have the *actual* secret value, just the name of
// the secret which was specified as a build secret.
SSH string `yaml:"ssh,omitempty" json:"ssh,omitempty"`
// KnownHosts is the SSH known hosts data to use for host key verification.
// This should be the actual known hosts content (can be expanded from build args).
// When provided, SSH connections will verify the host key against this data.
// When not provided, BuildKit will use TOFU (Trust On First Use).
KnownHosts string `yaml:"knownHosts,omitempty" json:"knownHosts,omitempty"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't consider KnownHosts as auth.
Perthaps stick this dirrectly into SourceGit as SSHKnownHosts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're absolutely right! I've moved the KnownHosts field from GitAuth to SourceGit as SSHKnownHosts. This makes much more sense since known hosts are about host verification rather than authentication.

The changes include:

  • Moved KnownHostsSSHKnownHosts in SourceGit struct
  • Updated baseState() to use BuildKit's llb.KnownSSHHosts() option directly
  • Modified processBuildArgs() to handle the new field location
  • Updated tests and documentation accordingly

All tests pass with this refactoring. Thanks for the great feedback! (commit 2a458b3)

}

type GomodGitAuth struct {
Expand Down Expand Up @@ -81,6 +86,10 @@ func (a *GitAuth) SetGitOption(gi *llb.GitInfo) {
if a.SSH != "" {
gi.MountSSHSock = a.SSH
}

if a.KnownHosts != "" {
gi.KnownSSHHosts = a.KnownHosts
}
}

func (src *SourceGit) IsDir() bool {
Expand Down Expand Up @@ -156,7 +165,17 @@ func (src *SourceGit) processBuildArgs(lex *shell.Lex, args map[string]string, a
if err != nil {
errs = append(errs, err)
}
if len(errs) > 1 {

// Process KnownHosts in Auth if present
if src.Auth.KnownHosts != "" {
updated, err = expandArgs(lex, src.Auth.KnownHosts, args, allowArg)
src.Auth.KnownHosts = updated
if err != nil {
errs = append(errs, err)
}
}

if len(errs) > 0 {
Copy link
Preview

Copilot AI Aug 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition was changed from len(errs) > 1 to len(errs) > 0, but this changes the existing behavior. The original condition only returned an error when there were multiple errors, suggesting that a single error might be acceptable or handled differently. This change could break existing functionality.

Suggested change
if len(errs) > 0 {
if len(errs) > 1 {

Copilot uses AI. Check for mistakes.

return fmt.Errorf("failed to process build args for git source: %w", stderrors.Join(errs...))
}
return nil
Expand Down
68 changes: 68 additions & 0 deletions source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,22 @@ func TestSourceGitSSH(t *testing.T) {
checkGitOp(t, ops, &src)
})

t.Run("with known hosts", func(t *testing.T) {
knownHosts := "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7"
src := Source{
Git: &SourceGit{
URL: fmt.Sprintf("user@%s:test.git", addr),
Commit: t.Name(),
Auth: GitAuth{
KnownHosts: knownHosts,
},
},
}

ops := getSourceOp(ctx, t, src)
checkGitOp(t, ops, &src)
})

}

func TestSourceGitHTTP(t *testing.T) {
Expand Down Expand Up @@ -1031,6 +1047,11 @@ func checkGitOp(t *testing.T, ops []*pb.Op, src *Source) {
}
assert.Check(t, cmp.Equal(op.Attrs["git.mountsshsock"], ssh), op)
}

// Check known hosts if set
if src.Git.Auth.KnownHosts != "" {
assert.Check(t, cmp.Equal(op.Attrs["git.knownsshhosts"], src.Git.Auth.KnownHosts), op.Attrs)
}
}

func checkGitAuth(t *testing.T, m map[string]*pb.Op, ops []*pb.Op, src *Source, expectedNumSecrets, expectedNumSSH int) {
Expand Down Expand Up @@ -1377,3 +1398,50 @@ func Test_pathHasPrefix(t *testing.T) {
})
}
}

// Test GitAuth SetGitOption method specifically for KnownHosts
func TestGitAuthSetGitOption(t *testing.T) {
tests := []struct {
name string
auth *GitAuth
expectKnownHosts string
}{
{
name: "nil auth",
auth: nil,
expectKnownHosts: "",
},
{
name: "empty known hosts",
auth: &GitAuth{
KnownHosts: "",
},
expectKnownHosts: "",
},
{
name: "with known hosts",
auth: &GitAuth{
KnownHosts: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7",
},
expectKnownHosts: "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7",
},
{
name: "multiline known hosts",
auth: &GitAuth{
KnownHosts: `github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI`,
},
expectKnownHosts: `github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7
gitlab.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gi := &llb.GitInfo{}
tt.auth.SetGitOption(gi)

assert.Check(t, cmp.Equal(gi.KnownSSHHosts, tt.expectKnownHosts))
})
}
}
36 changes: 36 additions & 0 deletions website/docs/sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,42 @@ git auth section (shown below with default values):
Note: These are secret names which are used to reference the secrets provided
by the client, not the actual secret values.

#### SSH Known Hosts

For SSH-based git URLs, you can specify known SSH host keys to improve security
and avoid Trust-On-First-Use (TOFU) behavior. When known hosts are provided,
the SSH connection will verify the host key against the specified keys and fail
if they don't match.

```yaml
someSource1:
git:
url: [email protected]:myOrg/myRepo.git
commit: 1234567890abcdef
auth:
knownHosts: |
github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqbLJofwIHMHnSVPP0k+aLU6X5OtN6a1r9K4kS...
github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
```

You can also use build arguments to dynamically provide known hosts:

```yaml
someSource1:
git:
url: [email protected]:myOrg/myRepo.git
commit: 1234567890abcdef
auth:
knownHosts: ${KNOWN_HOSTS}
```

Then build with: `docker build --build-arg KNOWN_HOSTS="$(cat ~/.ssh/known_hosts)" ...`

When known hosts are not specified, BuildKit will use Trust-On-First-Use (TOFU)
behavior, where it will connect to the SSH server, retrieve the host key, and
use that for the connection. Specifying known hosts avoids this extra connection
and provides better security by ensuring you're connecting to the expected server.

### HTTP

HTTP sources fetch a file from an HTTP URL. The HTTP source type is considered to be a "file" source.
Expand Down