Skip to content
Clone

fix(storage,chart): sign presigned URLs with the public endpoint

Presigned URLs embed the endpoint host in their SigV4 signature, so signing
with the in-cluster service address produced links clients could never
resolve -- and leaked the internal DNS name. This affected every presigned
URL, not just avatars: user avatars, Git LFS transfers, repo archives, CI
artifacts, chat attachment uploads and the admin storage browser.

- config: add `s3.public_endpoint` (falls back to `endpoint` when unset)
- storage: keep a second bucket handle pointed at the public endpoint and
  route all `presign_*` calls through it, so internal reads/writes stay on
  the cluster-local endpoint while signed links use the public host
- chart: mount the repos PVC into apihost -- its tree/commits endpoints read
  the bare repositories from disk and were returning 500 without it
ZhenYi committed
parent a79b908 commit 4143c48 Browse files

4 files changed

+54 −6

File tree 4
Modified chart/templates/apihost.yaml View chart/templates/apihost.yaml
@@ -84,6 +84,20 @@
8484 {{- toYaml .Values.securityContext | nindent 12 }}
8585 resources:
8686 {{- toYaml .Values.apihost.resources | nindent 12 }}
87 volumeMounts:
88 - name: repos
89 mountPath: {{ .Values.persistence.repos.mountPath }}
90 volumes:
91 - name: repos
92 {{- if .Values.persistence.repos.existingClaim }}
93 persistentVolumeClaim:
94 claimName: {{ .Values.persistence.repos.existingClaim }}
95 {{- else if .Values.persistence.repos.enabled }}
96 persistentVolumeClaim:
97 claimName: {{ printf "%s-repos" (include "gitdata.fullname" .) }}
98 {{- else }}
99 emptyDir: {}
100 {{- end }}
87101---
88102apiVersion: v1
89103kind: Service
Modified config/default.toml View config/default.toml
@@ -2,6 +2,8 @@
22bucket = ""
33region = "us-east-1"
44endpoint = ""
5# Presigned URL 使用的对外端点(浏览器/客户端必须可达)。留空则沿用 endpoint。
6public_endpoint = ""
57access_key_id = ""
68secret_access_key = ""
79path_style = false
Modified lib/config/src/s3.rs View lib/config/src/s3.rs
@@ -8,6 +8,11 @@
88 pub bucket: Option<String>,
99 pub region: Option<String>,
1010 pub endpoint: Option<String>,
11 /// Endpoint used when signing URLs handed to clients (browser downloads,
12 /// avatar links, LFS transfers). Presigned URLs embed the endpoint host in
13 /// their signature, so signing with an in-cluster address produces links
14 /// no client can resolve. Defaults to `endpoint` when unset.
15 pub public_endpoint: Option<String>,
1116 pub access_key_id: Option<String>,
1217 pub secret_access_key: Option<String>,
1318 pub path_style: Option<bool>,
@@ -36,6 +41,15 @@
3641 self.region.as_deref().unwrap_or("us-east-1")
3742 }
3843
44 /// Endpoint presigned URLs must be addressed to, falling back to the
45 /// internal endpoint when no public one is configured.
46 pub fn presign_endpoint(&self) -> Option<&str> {
47 self.public_endpoint
48 .as_deref()
49 .filter(|s| !s.is_empty())
50 .or_else(|| self.endpoint.as_deref().filter(|s| !s.is_empty()))
51 }
52
3953 pub fn use_path_style(&self) -> bool {
4054 self.path_style.unwrap_or(false)
4155 }
Modified lib/storage/src/s3.rs View lib/storage/src/s3.rs
@@ -45,6 +45,11 @@
4545#[derive(Clone)]
4646pub struct S3Client {
4747 bucket: Bucket,
48 /// Same bucket, but pointed at `s3.public_endpoint`. Presigned URLs carry
49 /// the endpoint host inside their signature, so they must be produced by a
50 /// client whose region points at the address the *caller* can reach (eg.
51 /// https://s3.gitdata.ai) rather than the in-cluster service DNS name.
52 presign_bucket: Bucket,
4853 bucket_name: String,
4954}
5055
@@ -63,8 +68,21 @@
6368 bucket = bucket.with_request_timeout(timeout);
6469 }
6570
71 let mut presign_bucket = bucket.clone();
72 if let Some(public_endpoint) = config
73 .public_endpoint
74 .as_deref()
75 .filter(|s| !s.is_empty())
76 {
77 presign_bucket.region = Region::Custom {
78 region: config.region().to_string(),
79 endpoint: public_endpoint.to_string(),
80 };
81 }
82
6683 Ok(Self {
6784 bucket,
85 presign_bucket,
6886 bucket_name: bucket_name.to_string(),
6987 })
7088 }
@@ -342,7 +360,7 @@
342360
343361 pub fn presign_get(&self, key: &str, expiry_secs: u32) -> Result<String, StorageError> {
344362 check_expiry(expiry_secs)?;
345 Ok(self.bucket.presign_get(key, expiry_secs, None)?)
363 Ok(self.presign_bucket.presign_get(key, expiry_secs, None)?)
346364 }
347365
348366 pub fn presign_get_with_queries(
@@ -353,13 +371,13 @@
353371 ) -> Result<String, StorageError> {
354372 check_expiry(expiry_secs)?;
355373 Ok(self
356 .bucket
374 .presign_bucket
357375 .presign_get(key, expiry_secs, Some(custom_queries))?)
358376 }
359377
360378 pub fn presign_put(&self, key: &str, expiry_secs: u32) -> Result<String, StorageError> {
361379 check_expiry(expiry_secs)?;
362 Ok(self.bucket.presign_put(key, expiry_secs, None)?)
380 Ok(self.presign_bucket.presign_put(key, expiry_secs, None)?)
363381 }
364382
365383 pub fn presign_put_with_headers(
@@ -370,13 +388,13 @@
370388 ) -> Result<String, StorageError> {
371389 check_expiry(expiry_secs)?;
372390 Ok(self
373 .bucket
391 .presign_bucket
374392 .presign_put(key, expiry_secs, Some(custom_headers))?)
375393 }
376394
377395 pub fn presign_delete(&self, key: &str, expiry_secs: u32) -> Result<String, StorageError> {
378396 check_expiry(expiry_secs)?;
379 Ok(self.bucket.presign_delete(key, expiry_secs)?)
397 Ok(self.presign_bucket.presign_delete(key, expiry_secs)?)
380398 }
381399
382400 pub fn presign_post(
@@ -386,7 +404,7 @@
386404 post_policy: String,
387405 ) -> Result<String, StorageError> {
388406 check_expiry(expiry_secs)?;
389 Ok(self.bucket.presign_post(key, expiry_secs, post_policy)?)
407 Ok(self.presign_bucket.presign_post(key, expiry_secs, post_policy)?)
390408 }
391409}
392410