feat(s3): parse and strictly validate redirectendpoint on driver initialization (#4870)

This commit is contained in:
Milos Gajdos
2026-05-31 12:31:37 -07:00
committed by GitHub
2 changed files with 43 additions and 0 deletions

View File

@@ -18,6 +18,7 @@ Amazon S3 or S3 compatible services for object storage.
| `secretkey` | no | Your AWS Secret Key. If you use [IAM roles](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html), omit to fetch temporary credentials from IAM. |
| `region` | yes | The AWS region in which your bucket exists. |
| `regionendpoint` | no | Endpoint for S3 compatible storage services (Minio, etc). |
| `redirectendpoint` | no | The public-facing endpoint used exclusively for generating presigned redirect URLs for object downloads. |
| `forcepathstyle` | no | To enable path-style addressing when the value is set to `true`. The default is `false`. |
| `bucket` | yes | The bucket name in which you want to store the registry's data. |
| `encrypt` | no | Specifies whether the registry stores the image in encrypted format or not. A boolean value. The default is `false`. |
@@ -47,6 +48,9 @@ Amazon S3 or S3 compatible services for object storage.
`regionendpoint`: (optional) Endpoint URL for S3 compatible APIs, from version 3+ it's required to be used with `forcepathstyle: true`. Given the `regionendpoint` overrides the API host domain, forcing the path style is necessary, see [more about](https://github.com/distribution/distribution/issues/4528). **This option should not be provided when using Amazon S3.**
`redirectendpoint`: (optional) A public-facing S3 or CDN endpoint URL used exclusively for generating presigned redirect URLs for object downloads. This is useful in architectures where image uploads travel over an isolated internal network endpoint (`regionendpoint`), while image downloads are routed through a separate, accelerated public endpoint. Must include a valid scheme (`http://` or `https://`) and host, and cannot contain a base path or query parameters. Note that replacing the host only works seamlessly for path-style addressing, effectively requiring `forcepathstyle: true` if using custom non-S3 domains.
`forcepathstyle`: (optional) Force path style for S3 compatible APIs. Some manufacturers only support force path style, while others only support DNS based bucket routing. Amazon S3 supports both. The value of this parameter applies, regardless of the region settings.
`bucket`: The name of your S3 bucket where you wish to store objects. The bucket must exist prior to the driver initialization.

View File

@@ -20,6 +20,7 @@ import (
"io"
"math"
"net/http"
"net/url"
"path/filepath"
"slices"
"sort"
@@ -117,6 +118,7 @@ type DriverParameters struct {
Accelerate bool
UseFIPSEndpoint bool
LogLevel aws.LogLevelType
RedirectEndpoint string
}
func init() {
@@ -165,6 +167,7 @@ type driver struct {
RootDirectory string
StorageClass string
ObjectACL string
RedirectEndpoint *url.URL
pool *sync.Pool
}
@@ -335,6 +338,11 @@ func FromParameters(ctx context.Context, parameters map[string]any) (*Driver, er
return nil, err
}
redirectEndpoint := parameters["redirectendpoint"]
if redirectEndpoint == nil {
redirectEndpoint = ""
}
params := DriverParameters{
AccessKey: fmt.Sprint(accessKey),
SecretKey: fmt.Sprint(secretKey),
@@ -360,6 +368,7 @@ func FromParameters(ctx context.Context, parameters map[string]any) (*Driver, er
Accelerate: accelerateBool,
UseFIPSEndpoint: useFIPSEndpointBool,
LogLevel: getS3LogLevelFromParam(parameters["loglevel"]),
RedirectEndpoint: fmt.Sprint(redirectEndpoint),
}
return New(ctx, params)
@@ -537,6 +546,29 @@ func New(ctx context.Context, params DriverParameters) (*Driver, error) {
},
}
if params.RedirectEndpoint != "" {
u, err := url.Parse(params.RedirectEndpoint)
if err != nil {
return nil, fmt.Errorf("unable to parse redirectendpoint: %w", err)
}
if u.Scheme == "" {
return nil, fmt.Errorf("no scheme specified for redirectendpoint")
}
if u.Host == "" {
return nil, fmt.Errorf("no host specified for redirectendpoint")
}
// Edge Case: Reject trailing paths (e.g., /cdn/v1) because the hot path ignores them
if u.Path != "" && u.Path != "/" {
return nil, fmt.Errorf("redirectendpoint cannot contain a base path: %s", u.Path)
}
// Edge Case: Reject query parameters because they break AWS SigV4 signatures
if u.RawQuery != "" {
return nil, fmt.Errorf("redirectendpoint cannot contain query parameters")
}
d.RedirectEndpoint = u
}
return &Driver{
baseEmbed: baseEmbed{
Base: base.Base{
@@ -1041,6 +1073,13 @@ func (d *driver) RedirectURL(r *http.Request, path string) (string, error) {
return "", nil
}
// If a public redirect endpoint is configured, use it for the signed URL
// This allows using a different public endpoint for downloads with signed URLs
if d.RedirectEndpoint != nil && req.HTTPRequest != nil {
req.HTTPRequest.URL.Host = d.RedirectEndpoint.Host
req.HTTPRequest.URL.Scheme = d.RedirectEndpoint.Scheme
}
return req.Presign(expiresIn)
}