Skip to content
Open
Show file tree
Hide file tree
Changes from all 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: 21 additions & 0 deletions docs/spec.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1020,6 +1020,17 @@
"$ref": "#/$defs/GomodGitAuthSSH",
"description": "SSH is a struct container the name of the ssh ID which contains the\naddress of the ssh auth socket, plus the username to use for the git\nremote.\nNote: This should not have the *actual* socket address, just the name of\nthe ssh ID which was specified as a build secret."
},
"sshKnownHosts": {
"items": {
"type": [
"string"
]
},
"type": [
"array"
],
"description": "SSHKnownHosts is a list of SSH host keys for host verification when using SSH auth.\nThese are used to prevent man-in-the-middle attacks during git operations.\nEach entry should be in the format: \"hostname ssh-keytype key\""
},
"token": {
"type": [
"string",
Expand Down Expand Up @@ -1680,6 +1691,16 @@
"boolean"
]
},
"sshKnownHosts": {
"items": {
"type": [
"string"
]
},
"type": [
"array"
]
},
"url": {
"type": [
"string"
Expand Down
56 changes: 54 additions & 2 deletions generator_gomod.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ func (g *GeneratorGomod) processBuildArgs(args map[string]string, allowArg func(
continue
}

// Process SSH known hosts for build arg expansion
if len(auth.SSHKnownHosts) > 0 {
for i, knownHost := range auth.SSHKnownHosts {
updated, err := expandArgs(lex, knownHost, args, allowArg)
if err != nil {
errs = append(errs, err)
continue
}
auth.SSHKnownHosts[i] = updated
}
}

g.Auth[subbed] = auth
if subbed != host {
delete(g.Auth, host)
Expand Down Expand Up @@ -61,6 +73,21 @@ func (s *Spec) HasGomods() bool {
return false
}

func (g *SourceGenerator) getGitSSHCommand() string {
if g.Gomod == nil {
return "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
}

// Check if any auth has SSH known hosts
for _, auth := range g.Gomod.Auth {
if len(auth.SSHKnownHosts) > 0 {
return "ssh"
}
}

return "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"
}

func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, credHelper llb.RunOption, opts ...llb.ConstraintsOpt) func(llb.State) llb.State {
return func(in llb.State) llb.State {
const (
Expand Down Expand Up @@ -95,7 +122,7 @@ func withGomod(g *SourceGenerator, srcSt, worker llb.State, subPath string, cred
llb.AddEnv("GOPATH", "/go"),
credHelper,
llb.AddEnv("TMP_GOMODCACHE", proxyPath),
llb.AddEnv("GIT_SSH_COMMAND", "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no"),
llb.AddEnv("GIT_SSH_COMMAND", g.getGitSSHCommand()),
llb.Dir(filepath.Join(joinedWorkDir, path)),
srcMount,
llb.AddMount(proxyPath, llb.Scratch(), llb.AsPersistentCacheDir(GomodCacheKey, llb.CacheMountShared)),
Expand All @@ -118,6 +145,31 @@ func (g *SourceGenerator) gitconfigGeneratorScript(scriptPath string) llb.State

goPrivate := []string{}

// First, set up SSH known hosts if any exist
hasKnownHosts := false
for _, host := range sortedHosts {
auth := g.Gomod.Auth[host]
if len(auth.SSHKnownHosts) > 0 {
hasKnownHosts = true
break
}
}

if hasKnownHosts {
fmt.Fprintln(&script, `# Setup SSH known hosts`)
fmt.Fprintln(&script, `mkdir -p ~/.ssh`)
fmt.Fprintln(&script, `chmod 700 ~/.ssh`)
for _, host := range sortedHosts {
auth := g.Gomod.Auth[host]
for _, knownHost := range auth.SSHKnownHosts {
fmt.Fprintf(&script, `echo "%s" >> ~/.ssh/known_hosts`, knownHost)
script.WriteRune('\n')
}
}
fmt.Fprintln(&script, `chmod 600 ~/.ssh/known_hosts`)
script.WriteRune('\n')
}

for _, host := range sortedHosts {
auth := g.Gomod.Auth[host]
gpHost, _, _ := strings.Cut(host, ":")
Expand Down Expand Up @@ -185,7 +237,7 @@ func (g *SourceGenerator) withGomodSecretsAndSockets() llb.RunOption {

llb.AddEnv(
"GIT_SSH_COMMAND",
`ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no`,
g.getGitSSHCommand(),
).SetRunOption(ei)
}
}
Expand Down
17 changes: 13 additions & 4 deletions source.go
Original file line number Diff line number Diff line change
Expand Up @@ -254,18 +254,18 @@ func Sources(spec *Spec, sOpt SourceOpts, opts ...llb.ConstraintsOpt) (map[strin
return states, nil
}

func (g *SourceGenerator) fillDefaults(host string, authInfo *GitAuth) {
func (g *SourceGenerator) fillDefaults(host string, authInfo *GitAuth, sshKnownHosts []string) {
if g == nil || authInfo == nil {
return
}

switch {
case g.Gomod != nil:
g.Gomod.fillDefaults(host, authInfo)
g.Gomod.fillDefaults(host, authInfo, sshKnownHosts)
}
}

func (gm *GeneratorGomod) fillDefaults(host string, authInfo *GitAuth) {
func (gm *GeneratorGomod) fillDefaults(host string, authInfo *GitAuth, sshKnownHosts []string) {
// Don't overwrite explicitly-specified auth
_, ok := gm.Auth[host]
if ok {
Expand All @@ -285,7 +285,16 @@ func (gm *GeneratorGomod) fillDefaults(host string, authInfo *GitAuth) {
Username: defaultUsername,
}
default:
return
// If no auth is specified but we have SSH known hosts, still create an entry
if len(sshKnownHosts) == 0 {
return
}
}

// Copy SSH known hosts if available
if len(sshKnownHosts) > 0 {
gomodAuth.SSHKnownHosts = make([]string, len(sshKnownHosts))
copy(gomodAuth.SSHKnownHosts, sshKnownHosts)
}

if gm.Auth == nil {
Expand Down
33 changes: 26 additions & 7 deletions source_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,11 @@ import (
)

type SourceGit struct {
URL string `yaml:"url" json:"url"`
Commit string `yaml:"commit" json:"commit"`
KeepGitDir bool `yaml:"keepGitDir,omitempty" json:"keepGitDir,omitempty"`
Auth GitAuth `yaml:"auth,omitempty" json:"auth,omitempty"`
URL string `yaml:"url" json:"url"`
Commit string `yaml:"commit" json:"commit"`
KeepGitDir bool `yaml:"keepGitDir,omitempty" json:"keepGitDir,omitempty"`
Auth GitAuth `yaml:"auth,omitempty" json:"auth,omitempty"`
SSHKnownHosts []string `yaml:"sshKnownHosts,omitempty" json:"sshKnownHosts,omitempty"`
}

type GitAuth struct {
Expand Down Expand Up @@ -53,6 +54,10 @@ type GomodGitAuth struct {
// Note: This should not have the *actual* socket address, just the name of
// the ssh ID which was specified as a build secret.
SSH *GomodGitAuthSSH `yaml:"ssh,omitempty" json:"ssh,omitempty"`
// SSHKnownHosts is a list of SSH host keys for host verification when using SSH auth.
// These are used to prevent man-in-the-middle attacks during git operations.
// Each entry should be in the format: "hostname ssh-keytype key"
SSHKnownHosts []string `yaml:"sshKnownHosts,omitempty" json:"sshKnownHosts,omitempty"`
}

type GomodGitAuthSSH struct {
Expand Down Expand Up @@ -116,6 +121,10 @@ func (src *SourceGit) baseState(opts fetchOptions) llb.State {
gOpts = append(gOpts, WithConstraints(opts.Constraints...))
gOpts = append(gOpts, &src.Auth)

for _, host := range src.SSHKnownHosts {
gOpts = append(gOpts, llb.KnownSSHHosts(host))
}

return llb.Git(src.URL, src.Commit, gOpts...)
}

Expand All @@ -135,10 +144,10 @@ func (git *SourceGit) fillDefaults(ls []*SourceGenerator) {
host = u.Host
}

// Thes the git auth from the git source is autofilled for the gomods, so
// The git auth and SSH known hosts from the git source are autofilled for the gomods, so
// the user doesn't have to repeat themselves.
for _, gen := range ls {
gen.fillDefaults(host, &git.Auth)
gen.fillDefaults(host, &git.Auth, git.SSHKnownHosts)
}
}

Expand All @@ -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 SSH known hosts for build arg expansion
for i, host := range src.SSHKnownHosts {
updated, err := expandArgs(lex, host, args, allowArg)
src.SSHKnownHosts[i] = updated
if err != nil {
errs = append(errs, err)
}
}

if len(errs) > 0 {
return fmt.Errorf("failed to process build args for git source: %w", stderrors.Join(errs...))
}
return nil
Expand Down
56 changes: 52 additions & 4 deletions source_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,20 @@ 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(),
SSHKnownHosts: []string{knownHosts},
},
}

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

}

func TestSourceGitHTTP(t *testing.T) {
Expand Down Expand Up @@ -177,14 +191,20 @@ func TestSourceGitHTTP(t *testing.T) {

t.Run("gomod auth", func(t *testing.T) {
const (
numSecrets = 2
numSecrets = 3 // Updated to account for localhost auto-filled auth
numSSH = 1
)

knownHosts := []string{
"github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbZ...",
"dev.azure.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...",
}

src := Source{
Git: &SourceGit{
URL: "https://localhost/test.git",
Commit: t.Name(),
URL: "https://localhost/test.git",
Commit: t.Name(),
SSHKnownHosts: knownHosts,
Auth: GitAuth{
Header: "some header",
},
Expand Down Expand Up @@ -218,8 +238,24 @@ func TestSourceGitHTTP(t *testing.T) {
},
}

// Check that SSH known hosts are properly copied during fillDefaults
srcInSpec := spec.Sources[srcName]
srcInSpec.fillDefaults()
spec.Sources[srcName] = srcInSpec
srcAfterDefaults := spec.Sources[srcName]

// Verify that SSH known hosts were propagated to localhost (from git URL)
localhostAuth, ok := srcAfterDefaults.Generate[0].Gomod.Auth["localhost"]
if !ok {
t.Fatal("Expected git auth to be auto-filled for localhost from git source URL")
}

if !reflect.DeepEqual(localhostAuth.SSHKnownHosts, knownHosts) {
t.Fatalf("SSH known hosts not properly propagated. Expected %v, got %v", knownHosts, localhostAuth.SSHKnownHosts)
}

m, ops := getGomodLLBOps(ctx, t, spec)
checkGitAuth(t, m, ops, &src, numSecrets, numSSH)
checkGitAuth(t, m, ops, &srcAfterDefaults, numSecrets, numSSH)
})
}

Expand Down Expand Up @@ -1061,6 +1097,18 @@ 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 len(src.Git.SSHKnownHosts) > 0 {
// BuildKit's KnownSSHHosts option may add formatting like newlines
actualKnownHosts := op.Attrs["git.knownsshhosts"]
expectedKnownHosts := strings.Join(src.Git.SSHKnownHosts, "\n")

// Remove trailing whitespace for comparison since BuildKit may add formatting
actualTrimmed := strings.TrimSpace(actualKnownHosts)
expectedTrimmed := strings.TrimSpace(expectedKnownHosts)
assert.Check(t, cmp.Equal(actualTrimmed, expectedTrimmed), "Expected: %q, Got: %q", expectedKnownHosts, actualKnownHosts)
}
}

func checkGitAuth(t *testing.T, m map[string]*pb.Op, ops []*pb.Op, src *Source, expectedNumSecrets, expectedNumSSH int) {
Expand Down
Loading