Files
zot/pkg/extensions/extension_image_trust.go
T
Ramkumar Chinchani 934b22d124 fix(security): enhance timeout configurations and body size limits fo… (#3984)
* fix(security): enhance timeout configurations and body size limits for HTTP requests

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(tests): refactor backend result handling in proxyHTTPRequest test

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): preserve ContentLength in proxied requests to prevent server hang

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): preserve explicit zero-length request bodies in proxyHTTPRequest
fix(tests): add test for normalizedTimeout function to ensure default fallback

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): prevent default HTTP timeout values from being set unless explicitly configured

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): refactor timeout handling to use explicit checks for nil and non-positive values

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(tests): add wait_for_event_count function to ensure expected event generation

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): improve timeout handling and update error responses for large requests

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): enhance HTTP timeout handling with explicit accessors and default values

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): increase default API key body size and timeout values for improved performance

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): unify timeout handling by replacing specific read/write timeouts with a single default timeout

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): consolidate HTTP timeout accessors and enhance timeout handling

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix(security): simplify HTTP timeout accessors and set default values for read/write timeouts

Co-authored-by: Copilot <copilot@github.com>
Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

---------

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-26 22:23:48 +03:00

215 lines
7.4 KiB
Go

//go:build imagetrust
package extensions
import (
"errors"
"io"
"net/http"
"time"
"github.com/gorilla/mux"
zerr "zotregistry.dev/zot/v2/errors"
"zotregistry.dev/zot/v2/pkg/api/config"
"zotregistry.dev/zot/v2/pkg/api/constants"
zcommon "zotregistry.dev/zot/v2/pkg/common"
"zotregistry.dev/zot/v2/pkg/extensions/imagetrust"
"zotregistry.dev/zot/v2/pkg/log"
mTypes "zotregistry.dev/zot/v2/pkg/meta/types"
"zotregistry.dev/zot/v2/pkg/scheduler"
sconstants "zotregistry.dev/zot/v2/pkg/storage/constants"
)
func IsBuiltWithImageTrustExtension() bool {
return true
}
func SetupImageTrustRoutes(conf *config.Config, router *mux.Router, metaDB mTypes.MetaDB, log log.Logger) {
extensionsConfig := conf.CopyExtensionsConfig()
if !extensionsConfig.IsImageTrustEnabled() ||
(!extensionsConfig.IsCosignEnabled() && !extensionsConfig.IsNotationEnabled()) {
log.Info().Msg("skip enabling the image trust routes as the config prerequisites are not met")
return
}
log.Info().Msg("setting up image trust routes")
imgTrustStore, _ := metaDB.ImageTrustStore().(*imagetrust.ImageTrustStore)
trust := ImageTrust{Conf: conf, ImageTrustStore: imgTrustStore, Log: log}
allowedMethods := zcommon.AllowedMethods(http.MethodPost)
if extensionsConfig.IsNotationEnabled() {
log.Info().Msg("setting up notation route")
notationRouter := router.PathPrefix(constants.ExtNotation).Subrouter()
notationRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
notationRouter.Use(zcommon.AddExtensionSecurityHeaders())
notationRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
// The endpoints for uploading signatures should be available only to admins
notationRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
notationRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleNotationCertificateUpload)
}
if extensionsConfig.IsCosignEnabled() {
log.Info().Msg("setting up cosign route")
cosignRouter := router.PathPrefix(constants.ExtCosign).Subrouter()
cosignRouter.Use(zcommon.CORSHeadersMiddleware(conf.HTTP.AllowOrigin))
cosignRouter.Use(zcommon.AddExtensionSecurityHeaders())
cosignRouter.Use(zcommon.ACHeadersMiddleware(conf, allowedMethods...))
// The endpoints for uploading signatures should be available only to admins
cosignRouter.Use(zcommon.AuthzOnlyAdminsMiddleware(conf))
cosignRouter.Methods(allowedMethods...).HandlerFunc(trust.HandleCosignPublicKeyUpload)
}
log.Info().Msg("finished setting up image trust routes")
}
type ImageTrust struct {
Conf *config.Config
ImageTrustStore *imagetrust.ImageTrustStore
Log log.Logger
}
// HandleCosignPublicKeyUpload godoc
// @Summary Upload cosign public keys for verifying signatures
// @Description Upload cosign public keys for verifying signatures
// @Router /v2/_zot/ext/cosign [post]
// @Accept octet-stream
// @Produce json
// @Param requestBody body string true "Public key content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request"
// @Failure 413 {string} string "request entity too large"
// @Failure 500 {string} string "internal server error"
func (trust *ImageTrust) HandleCosignPublicKeyUpload(response http.ResponseWriter, request *http.Request) {
body, err := io.ReadAll(http.MaxBytesReader(response, request.Body, constants.MaxImageTrustBodySize))
if err != nil {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
response.WriteHeader(http.StatusRequestEntityTooLarge)
} else {
trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to read cosign key body")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
err = imagetrust.UploadPublicKey(trust.ImageTrustStore.CosignStorage, body)
if err != nil {
if errors.Is(err, zerr.ErrInvalidPublicKeyContent) {
response.WriteHeader(http.StatusBadRequest)
} else {
trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to save cosign key")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
response.WriteHeader(http.StatusOK)
}
// HandleNotationCertificateUpload godoc
// @Summary Upload notation certificates for verifying signatures
// @Description Upload notation certificates for verifying signatures
// @Router /v2/_zot/ext/notation [post]
// @Accept octet-stream
// @Produce json
// @Param truststoreType query string false "truststore type"
// @Param requestBody body string true "Certificate content"
// @Success 200 {string} string "ok"
// @Failure 400 {string} string "bad request"
// @Failure 413 {string} string "request entity too large"
// @Failure 500 {string} string "internal server error"
func (trust *ImageTrust) HandleNotationCertificateUpload(response http.ResponseWriter, request *http.Request) {
var truststoreType string
if zcommon.QueryHasParams(request.URL.Query(), []string{"truststoreType"}) {
truststoreType = request.URL.Query().Get("truststoreType")
} else {
truststoreType = "ca" // default value of "truststoreType" query param
}
body, err := io.ReadAll(http.MaxBytesReader(response, request.Body, constants.MaxImageTrustBodySize))
if err != nil {
var mbe *http.MaxBytesError
if errors.As(err, &mbe) {
response.WriteHeader(http.StatusRequestEntityTooLarge)
} else {
trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to read notation certificate body")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
err = imagetrust.UploadCertificate(trust.ImageTrustStore.NotationStorage, body, truststoreType)
if err != nil {
if errors.Is(err, zerr.ErrInvalidTruststoreType) ||
errors.Is(err, zerr.ErrInvalidCertificateContent) {
response.WriteHeader(http.StatusBadRequest)
} else {
trust.Log.Error().Err(err).Str("component", "image-trust").Msg("failed to save notation certificate")
response.WriteHeader(http.StatusInternalServerError)
}
return
}
response.WriteHeader(http.StatusOK)
}
func EnableImageTrustVerification(conf *config.Config, taskScheduler *scheduler.Scheduler,
metaDB mTypes.MetaDB, log log.Logger,
) {
extensionsConfig := conf.CopyExtensionsConfig()
if !extensionsConfig.IsImageTrustEnabled() {
return
}
generator := imagetrust.NewTaskGenerator(metaDB, log)
numberOfHours := 2
interval := time.Duration(numberOfHours) * time.Hour
taskScheduler.SubmitGenerator(generator, interval, scheduler.MediumPriority)
}
func SetupImageTrustExtension(conf *config.Config, metaDB mTypes.MetaDB, log log.Logger) error {
extensionsConfig := conf.CopyExtensionsConfig()
if !extensionsConfig.IsImageTrustEnabled() {
return nil
}
var imgTrustStore mTypes.ImageTrustStore
var err error
if conf.Storage.RemoteCache && conf.Storage.CacheDriver["name"] == sconstants.DynamoDBDriverName {
// AWS secrets manager
// In case of AWS let's assume if dynamodDB is used, the AWS secrets manager is also used
// we use the CacheDriver settings as opposed to the storage settings because we want to
// be able to use S3/minio and redis in the same configuration
endpoint, _ := conf.Storage.CacheDriver["endpoint"].(string)
region, _ := conf.Storage.CacheDriver["region"].(string)
imgTrustStore, err = imagetrust.NewAWSImageTrustStore(region, endpoint)
if err != nil {
return err
}
} else {
// Store secrets on the local disk
imgTrustStore, err = imagetrust.NewLocalImageTrustStore(conf.Storage.RootDirectory)
if err != nil {
return err
}
}
metaDB.SetImageTrustStore(imgTrustStore)
return nil
}