feat(storage): add insecure_skip_tls_verify option for S3

Adds an opt-in toggle to skip TLS certificate verification when connecting
to the S3 endpoint, for self-hosted S3-compatible backends (e.g. rustfs,
MinIO) that use self-signed certificates. Exposed in both the store/API
protos and the storage settings UI, mirroring the existing use_path_style
toggle. When enabled, the AWS client uses an HTTP transport with
InsecureSkipVerify; default behavior is unchanged.

This governs backend-initiated S3 calls (uploads, deletes, thumbnails, and
image/document streaming). Video/audio playback redirects the browser to a
presigned URL, so that path still requires the browser to trust the cert.

Closes #6039
main
boojack 5 hours ago
parent 1e3ec38fa1
commit 20c19ef82d

@ -2,10 +2,13 @@ package s3
import (
"context"
"crypto/tls"
"io"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
@ -20,10 +23,20 @@ type Client struct {
}
func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client, error) {
cfg, err := config.LoadDefaultConfig(ctx,
loadOptions := []func(*config.LoadOptions) error{
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(s3Config.AccessKeyId, s3Config.AccessKeySecret, "")),
config.WithRegion(s3Config.Region),
)
}
if s3Config.InsecureSkipTlsVerify {
// Skip TLS certificate verification for endpoints using self-signed certificates.
// This is opt-in and removes protection against man-in-the-middle attacks.
httpClient := awshttp.NewBuildableClient().WithTransportOptions(func(tr *http.Transport) {
tr.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} // #nosec G402 -- opt-in for self-signed S3 endpoints
})
loadOptions = append(loadOptions, config.WithHTTPClient(httpClient))
}
cfg, err := config.LoadDefaultConfig(ctx, loadOptions...)
if err != nil {
return nil, errors.Wrap(err, "failed to load s3 config")
}

@ -184,6 +184,10 @@ message InstanceSetting {
string region = 4;
string bucket = 5;
bool use_path_style = 6;
// insecure_skip_tls_verify disables TLS certificate verification when connecting
// to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed
// certificate; it removes protection against man-in-the-middle attacks.
bool insecure_skip_tls_verify = 7;
}
// The S3 config.
S3Config s3_config = 4;

@ -1541,8 +1541,12 @@ type InstanceSetting_StorageSetting_S3Config struct {
Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"`
Bucket string `protobuf:"bytes,5,opt,name=bucket,proto3" json:"bucket,omitempty"`
UsePathStyle bool `protobuf:"varint,6,opt,name=use_path_style,json=usePathStyle,proto3" json:"use_path_style,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// insecure_skip_tls_verify disables TLS certificate verification when connecting
// to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed
// certificate; it removes protection against man-in-the-middle attacks.
InsecureSkipTlsVerify bool `protobuf:"varint,7,opt,name=insecure_skip_tls_verify,json=insecureSkipTlsVerify,proto3" json:"insecure_skip_tls_verify,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *InstanceSetting_StorageSetting_S3Config) Reset() {
@ -1617,6 +1621,13 @@ func (x *InstanceSetting_StorageSetting_S3Config) GetUsePathStyle() bool {
return false
}
func (x *InstanceSetting_StorageSetting_S3Config) GetInsecureSkipTlsVerify() bool {
if x != nil {
return x.InsecureSkipTlsVerify
}
return false
}
// Email delivery configuration for notifications.
type InstanceSetting_NotificationSetting_EmailSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
@ -1802,7 +1813,7 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\x06commit\x18\b \x01(\tR\x06commit\x12\x1f\n" +
"\vneeds_setup\x18\t \x01(\bR\n" +
"needsSetup\"\x1b\n" +
"\x19GetInstanceProfileRequest\"\xcd\x1b\n" +
"\x19GetInstanceProfileRequest\"\x86\x1c\n" +
"\x0fInstanceSetting\x12\x17\n" +
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" +
"\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" +
@ -1824,19 +1835,20 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
"\rCustomProfile\x12\x14\n" +
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
"\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" +
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x1a\xc1\x04\n" +
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x1a\xfa\x04\n" +
"\x0eStorageSetting\x12[\n" +
"\fstorage_type\x18\x01 \x01(\x0e28.memos.api.v1.InstanceSetting.StorageSetting.StorageTypeR\vstorageType\x12+\n" +
"\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" +
"\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x12R\n" +
"\ts3_config\x18\x04 \x01(\v25.memos.api.v1.InstanceSetting.StorageSetting.S3ConfigR\bs3Config\x1a\xd1\x01\n" +
"\ts3_config\x18\x04 \x01(\v25.memos.api.v1.InstanceSetting.StorageSetting.S3ConfigR\bs3Config\x1a\x8a\x02\n" +
"\bS3Config\x12\"\n" +
"\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12/\n" +
"\x11access_key_secret\x18\x02 \x01(\tB\x03\xe0A\x04R\x0faccessKeySecret\x12\x1a\n" +
"\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x16\n" +
"\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" +
"\x06bucket\x18\x05 \x01(\tR\x06bucket\x12$\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"L\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\x127\n" +
"\x18insecure_skip_tls_verify\x18\a \x01(\bR\x15insecureSkipTlsVerify\"L\n" +
"\vStorageType\x12\x1c\n" +
"\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" +
"\bDATABASE\x10\x01\x12\t\n" +

@ -3627,6 +3627,12 @@ components:
type: string
usePathStyle:
type: boolean
insecureSkipTlsVerify:
type: boolean
description: |-
insecure_skip_tls_verify disables TLS certificate verification when connecting
to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed
certificate; it removes protection against man-in-the-middle attacks.
description: |-
S3 configuration for cloud storage backend.
Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/

@ -673,8 +673,12 @@ type StorageS3Config struct {
Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"`
Bucket string `protobuf:"bytes,5,opt,name=bucket,proto3" json:"bucket,omitempty"`
UsePathStyle bool `protobuf:"varint,6,opt,name=use_path_style,json=usePathStyle,proto3" json:"use_path_style,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
// insecure_skip_tls_verify disables TLS certificate verification when connecting
// to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed
// certificate; it removes protection against man-in-the-middle attacks.
InsecureSkipTlsVerify bool `protobuf:"varint,7,opt,name=insecure_skip_tls_verify,json=insecureSkipTlsVerify,proto3" json:"insecure_skip_tls_verify,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StorageS3Config) Reset() {
@ -749,6 +753,13 @@ func (x *StorageS3Config) GetUsePathStyle() bool {
return false
}
func (x *StorageS3Config) GetInsecureSkipTlsVerify() bool {
if x != nil {
return x.InsecureSkipTlsVerify
}
return false
}
type InstanceMemoRelatedSetting struct {
state protoimpl.MessageState `protogen:"open.v1"`
// content_length_limit is the limit of content length. Unit is byte.
@ -1338,14 +1349,15 @@ const file_store_instance_setting_proto_rawDesc = "" +
"\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" +
"\bDATABASE\x10\x01\x12\t\n" +
"\x05LOCAL\x10\x02\x12\x06\n" +
"\x02S3\x10\x03\"\xd3\x01\n" +
"\x02S3\x10\x03\"\x8c\x02\n" +
"\x0fStorageS3Config\x12\"\n" +
"\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" +
"\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" +
"\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x16\n" +
"\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" +
"\x06bucket\x18\x05 \x01(\tR\x06bucket\x12$\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"\xc5\x01\n" +
"\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\x127\n" +
"\x18insecure_skip_tls_verify\x18\a \x01(\bR\x15insecureSkipTlsVerify\"\xc5\x01\n" +
"\x1aInstanceMemoRelatedSetting\x120\n" +
"\x14content_length_limit\x18\x03 \x01(\x05R\x12contentLengthLimit\x127\n" +
"\x18enable_double_click_edit\x18\x04 \x01(\bR\x15enableDoubleClickEdit\x12\x1c\n" +

@ -100,6 +100,10 @@ message StorageS3Config {
string region = 4;
string bucket = 5;
bool use_path_style = 6;
// insecure_skip_tls_verify disables TLS certificate verification when connecting
// to the S3 endpoint. Only enable this for trusted endpoints that use a self-signed
// certificate; it removes protection against man-in-the-middle attacks.
bool insecure_skip_tls_verify = 7;
}
message InstanceMemoRelatedSetting {

@ -444,10 +444,11 @@ func convertInstanceStorageSettingFromStore(settingpb *storepb.InstanceStorageSe
setting.S3Config = &v1pb.InstanceSetting_StorageSetting_S3Config{
AccessKeyId: settingpb.S3Config.AccessKeyId,
// AccessKeySecret is write-only: never returned in responses.
Endpoint: settingpb.S3Config.Endpoint,
Region: settingpb.S3Config.Region,
Bucket: settingpb.S3Config.Bucket,
UsePathStyle: settingpb.S3Config.UsePathStyle,
Endpoint: settingpb.S3Config.Endpoint,
Region: settingpb.S3Config.Region,
Bucket: settingpb.S3Config.Bucket,
UsePathStyle: settingpb.S3Config.UsePathStyle,
InsecureSkipTlsVerify: settingpb.S3Config.InsecureSkipTlsVerify,
}
}
return setting
@ -464,12 +465,13 @@ func convertInstanceStorageSettingToStore(setting *v1pb.InstanceSetting_StorageS
}
if setting.S3Config != nil {
settingpb.S3Config = &storepb.StorageS3Config{
AccessKeyId: setting.S3Config.AccessKeyId,
AccessKeySecret: setting.S3Config.AccessKeySecret,
Endpoint: setting.S3Config.Endpoint,
Region: setting.S3Config.Region,
Bucket: setting.S3Config.Bucket,
UsePathStyle: setting.S3Config.UsePathStyle,
AccessKeyId: setting.S3Config.AccessKeyId,
AccessKeySecret: setting.S3Config.AccessKeySecret,
Endpoint: setting.S3Config.Endpoint,
Region: setting.S3Config.Region,
Bucket: setting.S3Config.Bucket,
UsePathStyle: setting.S3Config.UsePathStyle,
InsecureSkipTlsVerify: setting.S3Config.InsecureSkipTlsVerify,
}
}
return settingpb

@ -146,6 +146,7 @@ const StorageSection = () => {
region: existing?.region ?? "",
bucket: existing?.bucket ?? "",
usePathStyle: existing?.usePathStyle ?? false,
insecureSkipTlsVerify: existing?.insecureSkipTlsVerify ?? false,
[field]: value,
}),
}),
@ -332,6 +333,16 @@ const StorageSection = () => {
onCheckedChange={(checked) => handleS3FieldChange("usePathStyle", checked)}
/>
</SettingRow>
<SettingRow
label={t("setting.storage.insecure-skip-tls-verify")}
description={t("setting.storage.insecure-skip-tls-verify-description")}
>
<Switch
checked={instanceStorageSetting.s3Config?.insecureSkipTlsVerify ?? false}
onCheckedChange={(checked) => handleS3FieldChange("insecureSkipTlsVerify", checked)}
/>
</SettingRow>
</SettingGroup>
)}

@ -680,6 +680,8 @@
"endpoint-description": "Service endpoint, such as an AWS S3, Cloudflare R2, MinIO, or other compatible URL.",
"filepath-template": "Filepath template",
"filepath-template-description": "Used by local and S3 storage. Supports {timestamp}, {uuid}, and {filename}. Default: assets/{timestamp}_{uuid}_{filename}.",
"insecure-skip-tls-verify": "Skip TLS verification",
"insecure-skip-tls-verify-description": "Disable TLS certificate verification for the S3 endpoint. Enable only for trusted endpoints using a self-signed certificate; this removes protection against man-in-the-middle attacks.",
"label": "Storage",
"local-description": "Store new attachments on the server file system. This is the default for new instances.",
"local-note-backup": "Persist and back up the attachment directory, especially in Docker or container deployments.",

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save