fix(submit): 审阅绑定上下文 sha256,批准须带摘要,构建只从内部 API 取上下文并校验字节
This commit is contained in:
30 files changed
+910
-98
No files matched your search
@@ -469,6 +469,9 @@ func buildConfig(cfg *config.Config) build.Config {
|
||||
// the internal DB mirror (see config.RegistryConfig.TrivyDBRepository).
|
||||
TrivyDBRepository: cfg.Registry.TrivyDBRepository,
|
||||
TrivyJavaDBRepository: cfg.Registry.TrivyJavaDBRepository,
|
||||
// The submit lane's derived context URLs live here; the fetch step's
|
||||
// service token goes nowhere else.
|
||||
ContextOrigin: internalAPIBaseURL(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -15,6 +17,8 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"felis.lolicon.best/internal/build"
|
||||
)
|
||||
|
||||
// cmdFetchContext is the in-Pod entrypoint the build Job's context-fetch
|
||||
@@ -41,9 +45,14 @@ func cmdFetchContext(args []string, _, stderr io.Writer) int {
|
||||
fs.SetOutput(stderr)
|
||||
url := fs.String("url", "", "internal-face URL of the submission's build-context tarball")
|
||||
out := fs.String("out", "/context", "directory to extract the build context into")
|
||||
want := fs.String("sha256", "", "refuse the context unless the tarball's sha256 is this lowercase hex digest")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return 2
|
||||
}
|
||||
if *want != "" && !build.IsSHA256Hex(*want) {
|
||||
fmt.Fprintf(stderr, "felis fetch-context: --sha256 %q is not a lowercase hex sha256\n", *want)
|
||||
return 2
|
||||
}
|
||||
if *url == "" {
|
||||
fmt.Fprintln(stderr, "felis fetch-context: --url is required")
|
||||
return 2
|
||||
@@ -66,7 +75,12 @@ func cmdFetchContext(args []string, _, stderr io.Writer) int {
|
||||
// No overall client timeout: a legitimate modpack context can be large and the
|
||||
// Job's activeDeadlineSeconds is the real bound. The header timeout catches a
|
||||
// wedged endpoint without capping a healthy download.
|
||||
client := &http.Client{Transport: &http.Transport{ResponseHeaderTimeout: time.Minute}}
|
||||
// Redirects are refused: the request carries the service token, and the
|
||||
// internal face never redirects, so a 3xx is someone steering the token.
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{ResponseHeaderTimeout: time.Minute},
|
||||
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse },
|
||||
}
|
||||
resp, err := fetchContextWithRetry(ctx, client, *url, token, stderr)
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "felis fetch-context: %v\n", err)
|
||||
@@ -74,10 +88,28 @@ func cmdFetchContext(args []string, _, stderr io.Writer) int {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if err := extractTarGz(resp.Body, *out); err != nil {
|
||||
h := sha256.New()
|
||||
body := io.TeeReader(resp.Body, h)
|
||||
if err := extractTarGz(body, *out); err != nil {
|
||||
fmt.Fprintf(stderr, "felis fetch-context: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if *want == "" {
|
||||
return 0
|
||||
}
|
||||
// The tar end marker comes before the gzip trailer and whatever follows it,
|
||||
// so read to EOF: the digest must cover every byte the blob holds. The blob
|
||||
// itself is size-capped at upload, which bounds this read.
|
||||
if _, err := io.Copy(io.Discard, io.LimitReader(body, maxContextBytes)); err != nil {
|
||||
fmt.Fprintf(stderr, "felis fetch-context: %v\n", err)
|
||||
return 1
|
||||
}
|
||||
if got := hex.EncodeToString(h.Sum(nil)); got != *want {
|
||||
// The init container failing is what keeps Kaniko from ever starting on
|
||||
// the extracted tree.
|
||||
fmt.Fprintf(stderr, "felis fetch-context: the context's sha256 is %s, the approved digest is %s: it changed after approval; refusing to build\n", got, *want)
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -187,6 +189,69 @@ func TestCmdFetchContextFetchAndExtract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// With --sha256 the fetch refuses any bytes but the approved ones, including
|
||||
// a tarball that extracts cleanly: that is exactly the context an uploader
|
||||
// swapped in after the review.
|
||||
func TestCmdFetchContextChecksDigest(t *testing.T) {
|
||||
body := tgzBody(t, tarEntry{name: "Dockerfile", body: "FROM scratch\n"})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(body)
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("FELIS_SERVICE_TOKEN", "test-token")
|
||||
sum := sha256.Sum256(body)
|
||||
good := hex.EncodeToString(sum[:])
|
||||
args := func(digest string) []string {
|
||||
return []string{"--url=" + srv.URL + "/sub-1/context", "--out=" + t.TempDir(), "--sha256=" + digest}
|
||||
}
|
||||
|
||||
if code := cmdFetchContext(args(good), io.Discard, io.Discard); code != 0 {
|
||||
t.Fatalf("matching digest exit = %d, want 0", code)
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
other := strings.Repeat("0", 64)
|
||||
if code := cmdFetchContext(args(other), io.Discard, &stderr); code != 1 || !strings.Contains(stderr.String(), "changed after approval") {
|
||||
t.Fatalf("mismatched digest exit = %d, stderr %q; want 1 naming the change", code, stderr.String())
|
||||
}
|
||||
stderr.Reset()
|
||||
if code := cmdFetchContext(args("ABC"), io.Discard, &stderr); code != 2 {
|
||||
t.Fatalf("malformed digest exit = %d, want 2 (stderr %q)", code, stderr.String())
|
||||
}
|
||||
|
||||
// Bytes after the tar end marker still count: appending to an approved blob
|
||||
// must change what the fetch accepts.
|
||||
padded := append(append([]byte{}, body...), "trailing"...)
|
||||
srvPadded := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write(padded)
|
||||
}))
|
||||
defer srvPadded.Close()
|
||||
if code := cmdFetchContext([]string{"--url=" + srvPadded.URL + "/c", "--out=" + t.TempDir(), "--sha256=" + good}, io.Discard, io.Discard); code != 1 {
|
||||
t.Fatalf("padded blob exit = %d, want 1", code)
|
||||
}
|
||||
}
|
||||
|
||||
// The request carries the service token, so a redirect is a failure: the token
|
||||
// never follows it to another host (build-supply-chain-13).
|
||||
func TestCmdFetchContextRefusesRedirects(t *testing.T) {
|
||||
var leaked bool
|
||||
elsewhere := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
leaked = true
|
||||
}))
|
||||
defer elsewhere.Close()
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, elsewhere.URL+"/steal", http.StatusFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
t.Setenv("FELIS_SERVICE_TOKEN", "test-token")
|
||||
var stderr bytes.Buffer
|
||||
if code := cmdFetchContext([]string{"--url=" + srv.URL + "/c", "--out=" + t.TempDir()}, io.Discard, &stderr); code != 1 {
|
||||
t.Fatalf("redirect exit = %d, want 1 (stderr %q)", code, stderr.String())
|
||||
}
|
||||
if leaked {
|
||||
t.Fatal("the fetch followed the redirect")
|
||||
}
|
||||
}
|
||||
|
||||
// A body that is not a gzip tarball must fail the extraction rather than produce
|
||||
// an empty (or partial) context Kaniko would then try to build.
|
||||
func TestExtractTarGzRejectsNonGzip(t *testing.T) {
|
||||
|
||||
Reference in new issue
Block a user