Global endpoint ambiguity

Hi there,

While exploring the creation of presigned urls in this context, I encountered an ambiguity in the global endpoint definition. On my Tigris dashboard I find two possible global endpoints:

  1. https://t3.storage.dev (default)
  2. https://fly.storage.tigris.dev (fly apps)

I am using Tigris via a Fly account, but I don’t use any Fly machines or the Fly network. I’ve tried creating presigned urls for both global endpoints, but only https://fly.storage.tigris.dev seems to work. When I use https://t3.storage.dev in my signing process then the upload request is rejected with a 403 - signature invalid response.

My questions:

  1. Does using https://fly.storage.tigris.dev as my global endpoint have any performance downsides when I don’t use any Fly machines / the Fly network?
  2. If so: could it be possible to use https://t3.storage.dev for all cases?

Thanks in advance!

Hi @hakoptak

Pre-signed URLs should be working for both the endpoints.

The fly.storage endpoint is optimized for users of Fly who have apps or machines deployed and are accessing Tigris from within those apps / machines.

The t3 endpoint is for everyone else.

Let me take a look at why you are seeing 403 when using the t3 endpoint with pre-signed URLs.

Hi @hakoptak

Can you share how did you generate presign URL for t3 endpoint? Presigned URL for t3 endpoints works fine and are supported.

For example:

 aws s3 --profile=jmj_org presign s3://jmj-candidate-bucket/2.txt

and

[jmj_org]
aws_access_key_id =tid_<>
aws_secret_access_key =tsec_<>
endpoint_url=https://t3.storage.dev
region=auto

Hi @jmj, I generate the presigned url on my server with the following parameters:

AccessKey: tid_<>
SecretKey: tsec_<>
Endpoint: see below
Region: auto
Bucket: test.sessions.pinggg.cloud
Method: PUT
Expiration: 15

When I use fly.storage.tigris.dev as endpoint then everything works fine, but when I use t3.storage.dev then I get the message:

<Error>
<Code>SignatureDoesNotMatch</Code>
<Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message>
<Resource>/dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png</Resource>
<RequestId>1750939451656676602</RequestId>
<Key>dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png</Key>
<BucketName>test.sessions.pinggg.cloud</BucketName>
</Error>

Below you find the parts that generate the presigned url in Golang. It’s the method PresignObject(...) that is called to generate the signature:

//---------------------------------------------------------------------------

const (
	algorithm = "AWS4-HMAC-SHA256"
	service   = "s3"
	request   = "aws4_request"
)

//---------------------------------------------------------------------------

type Presigner struct {
    AccessKey  string
    SecretKey  string
    Region     string
    Endpoint   string
    Bucket     string
    Method     string
    Expiration time.Duration
}

//---------------------------------------------------------------------------

// Creates a presigned GET/PUT date and signature
func (p *Presigner) PresignObject(objectName string) (string, []byte) {

	dateOnly, dateTime := presigner_Stamp()

	signature := p.PresignObjectWithDate(objectName, dateOnly, dateTime)

	return dateTime, signature

}

//---------------------------------------------------------------------------

// Creates a presigned GET/PUT signature
func (p *Presigner) PresignObjectWithDate(objectName string, dateOnly string, dateTime string) []byte {

	mime := presigner_ExtractMimeType(objectName)

	// Build canonical URI
	var canonicalURI strings.Builder

	canonicalURI.WriteByte('/')
	canonicalURI.WriteString(p.Bucket)
	canonicalURI.WriteByte('/')
	canonicalURI.WriteString(objectName)

	// Build credential string
	var credential strings.Builder

	// These '/'s should be escaped
	credential.WriteString(p.AccessKey)
	credential.WriteString("%2F")
	credential.WriteString(dateOnly)
	credential.WriteString("%2F")
	credential.WriteString(p.Region)
	credential.WriteString("%2F")
	credential.WriteString(service)
	credential.WriteString("%2F")
	credential.WriteString(request)

	// Build query parameters
	var canonicalQueryString strings.Builder

	// Add parameters in the correct order
	canonicalQueryString.WriteString(fmt.Sprintf("X-Amz-Algorithm=%s", algorithm))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Credential=%s", credential.String()))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Date=%s", dateTime))
	canonicalQueryString.WriteString(fmt.Sprintf("&X-Amz-Expires=%d", int(p.Expiration.Seconds())))
	canonicalQueryString.WriteString("&X-Amz-SignedHeaders=host")

	// Build host and canonical headers
	var canonicalHeaders strings.Builder

	canonicalHeaders.WriteString(fmt.Sprintf("host:%s", p.Endpoint))
	canonicalHeaders.WriteByte('\n')

	// Build canonical request
	var canonicalRequest strings.Builder

	canonicalRequest.WriteString(p.Method)
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalURI.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalQueryString.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString(canonicalHeaders.String())
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString("host")
	canonicalRequest.WriteByte('\n')
	canonicalRequest.WriteString("UNSIGNED-PAYLOAD")

	// Build string to sign
	var scope strings.Builder

	scope.WriteString(dateOnly)
	scope.WriteByte('/')
	scope.WriteString(p.Region)
	scope.WriteByte('/')
	scope.WriteString(service)
	scope.WriteByte('/')
	scope.WriteString(request)

	var stringToSign strings.Builder

	stringToSign.WriteString(algorithm)
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(dateTime)
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(scope.String())
	stringToSign.WriteByte('\n')
	stringToSign.WriteString(hex.EncodeToString(presigner_Hash([]byte(canonicalRequest.String()))))

	// Calculate signature
	signingKey := presigner_SignKey(p.SecretKey, dateOnly, p.Region, service)
	signature := presigner_Hmac(signingKey, []byte(stringToSign.String()))

	return signature

}

//---------------------------------------------------------------------------

func presigner_ExtractMimeType(objectName string) string {

	ext := filepath.Ext(objectName)

	return mime.TypeByExtension(ext)

}

//---------------------------------------------------------------------------

// Creates a date only string and a date with time string
func presigner_Stamp() (string, string) {

	datetime := time.Now().UTC()

	return datetime.Format("20060102"), datetime.Format("20060102T150405Z")

}

//---------------------------------------------------------------------------

func presigner_Hash(data []byte) []byte {

	h := sha256.New()

	h.Write(data)

	return h.Sum(nil)
}

//---------------------------------------------------------------------------

func presigner_Hmac(key []byte, data []byte) []byte {

	h := hmac.New(sha256.New, key)

	h.Write(data)

	return h.Sum(nil)

}

//---------------------------------------------------------------------------

func presigner_SignKey(secretKey string, date string, region string, service string) []byte {

	kDate := presigner_Hmac([]byte("AWS4"+secretKey), []byte(date))
	kRegion := presigner_Hmac(kDate, []byte(region))
	kService := presigner_Hmac(kRegion, []byte(service))

	return presigner_Hmac(kService, []byte(request))

}

//---------------------------------------------------------------------------

The signature and date are send to the client and assembled into:

https://test.sessions.pinggg.cloud/dFGR75OhGeGGRdNwyjIV3rOFMuxeBaNu8gus4mqSvGy/shares/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=tid_mNpmYFuMLHfstgsbJEnjDXQLjRSgqXlznLCJdlY_ZIhereWHco%2F20250626%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250626T120406Z&X-Amz-Expires=15&X-Amz-SignedHeaders=host&X-Amz-Signature=625a62854449727f57935b10caf1e9895c8ba0c7d424296c771b5e066f8b13bb

The client then uploads the file and gets the error listed above.

Could this be the result of me using a CNAME bucket mapped onto test.sessions.pinggg.cloud.fly.storage.tigris.dev?

Thanks in advance.

I just tried to replace the CNAME from test.sessions.pinggg.cloud.fly.storage.tigris.dev to test.sessions.pinggg.cloud.t3.storage.dev but now I get the error:

The certificate was not trusted.

Here is the generated url that gives the message:

https://test.sessions.pinggg.cloud/JaVwfmxm3TISljGp1f0PpzekfNv7CX3VkrzFLbs3IKFC/shares/1.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=tid_mNpmYFuMLHfstgsbJEnjDXQLjRSgqXlznLCJdlY_ZIhereWHco%2F20250626%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250626T124421Z&X-Amz-Expires=15&X-Amz-SignedHeaders=host&X-Amz-Signature=319c89942819d9fc4aadcfd86f6aad1b79e0f26a6014519e3cb52448346b1559

I’m out of ideas…

@hakoptak thank you for sharing the additional details. Please, for now, continue using the fly.storage.tigris.dev endpoint while we look into the issue.

Hi @ovaistariq, thanks for following up. That’s exactly what I thought as well.

I was just trying out the other endpoint and stumbled upon this problem, which I wanted to document for others as well. I hope you have enough details to investigate the issue.

So for now I’m switching back to the fly endpoint.

Hi @hakoptak

You can use presigned URL with path style access using t3 endpoint currently for GET and PUT. You can’t just yet use the custom domain with t3 - we will have an update for you later when we start to support the custom certificate on our t3 endpoint.

Here is an example Go code that you can use to get presigned URL using t3 endpoint

package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/aws/aws-sdk-go-v2/config"
	credentials2 "github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go/aws"
)

func main() {
	// Load AWS configuration
	cfg, err := config.LoadDefaultConfig(
		context.TODO(),
		config.WithRegion("auto"),
		config.WithCredentialsProvider(credentials2.NewStaticCredentialsProvider("tid_<>", "tsec_<>", "")),
	)
	if err != nil {
		log.Fatalf("unable to load SDK config, %v", err)
	}

	cfg.BaseEndpoint = aws.String("https://t3.storage.dev")

	// Create an S3 client
	s3Client := s3.NewFromConfig(cfg)

	// Define the input parameters for presigned URL
	bucket := "<>"
	objectKey := "<>"
	expiration := 5 * time.Minute // URL expiration time

	// Generate a presigned URL for the S3 object
	psClient := s3.NewPresignClient(s3Client)
	req, err := psClient.PresignGetObject(context.TODO(), &s3.GetObjectInput{
		Bucket: &bucket,
		Key:    &objectKey,
	}, s3.WithPresignExpires(expiration))
	if err != nil {
		log.Fatalf("unable to generate presigned URL, %v", err)
	}
	url := req.URL
	fmt.Printf("Presigned URL: %s\n", url)
}

Example resultant URL

https://t3.storage.dev/<bucket>/<object-key>X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Checksum-Mode=ENABLED&X-Amz-Credential=tid_<>%2Fauto%2Fs3%2Faws4_request&X-Amz-Date=20250627T184749Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&x-id=GetObject&X-Amz-Signature=<>

Hi @jmj, thanks for following up!

For now I’ll stick to the Fly endpoint. I will switch to the t3.storage.dev endpoint later. Sorry for hitting so many edge cases, but I want to configure Tigris as good as possible :nerd_face:

By the way: when I ping to fly.storage.tigris.dev and t3.storage.dev I find that the former responds twice as fast. As if there is an extra hub for t3.storage.dev

I’m in The Netherlands so that may be the reason.

This topic was automatically closed 7 days after the last reply. New replies are no longer allowed.