From 765526117becc5fb04afff22f5fdeef6e1a62a0f Mon Sep 17 00:00:00 2001 From: Adrien Date: Wed, 18 Feb 2026 13:01:55 +0100 Subject: [PATCH 1/2] Add chart helm --- .gitignore | 1 + k8s/transfer.sh/.helmignore | 23 ++ k8s/transfer.sh/Chart.yaml | 16 + k8s/transfer.sh/README.md | 334 ++++++++++++++++++ k8s/transfer.sh/templates/NOTES.txt | 42 +++ k8s/transfer.sh/templates/_helpers.tpl | 62 ++++ k8s/transfer.sh/templates/configmap.yaml | 100 ++++++ k8s/transfer.sh/templates/deployment.yaml | 130 +++++++ k8s/transfer.sh/templates/hpa.yaml | 32 ++ k8s/transfer.sh/templates/httproute.yaml | 37 ++ k8s/transfer.sh/templates/ingress.yaml | 41 +++ k8s/transfer.sh/templates/networkpolicy.yaml | 29 ++ k8s/transfer.sh/templates/pvc.yaml | 17 + k8s/transfer.sh/templates/service.yaml | 15 + k8s/transfer.sh/templates/serviceaccount.yaml | 13 + k8s/transfer.sh/values.yaml | 222 ++++++++++++ 16 files changed, 1114 insertions(+) create mode 100644 k8s/transfer.sh/.helmignore create mode 100644 k8s/transfer.sh/Chart.yaml create mode 100644 k8s/transfer.sh/README.md create mode 100644 k8s/transfer.sh/templates/NOTES.txt create mode 100644 k8s/transfer.sh/templates/_helpers.tpl create mode 100644 k8s/transfer.sh/templates/configmap.yaml create mode 100644 k8s/transfer.sh/templates/deployment.yaml create mode 100644 k8s/transfer.sh/templates/hpa.yaml create mode 100644 k8s/transfer.sh/templates/httproute.yaml create mode 100644 k8s/transfer.sh/templates/ingress.yaml create mode 100644 k8s/transfer.sh/templates/networkpolicy.yaml create mode 100644 k8s/transfer.sh/templates/pvc.yaml create mode 100644 k8s/transfer.sh/templates/service.yaml create mode 100644 k8s/transfer.sh/templates/serviceaccount.yaml create mode 100644 k8s/transfer.sh/values.yaml diff --git a/.gitignore b/.gitignore index f48732deb..23fc35423 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ bin/ *.pyc *.egg-info/ .idea/ +.vscode/ .tmp .vagrant diff --git a/k8s/transfer.sh/.helmignore b/k8s/transfer.sh/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/k8s/transfer.sh/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/k8s/transfer.sh/Chart.yaml b/k8s/transfer.sh/Chart.yaml new file mode 100644 index 000000000..867692046 --- /dev/null +++ b/k8s/transfer.sh/Chart.yaml @@ -0,0 +1,16 @@ +apiVersion: v2 +name: transfer-sh +description: A Helm chart for transfer.sh - Easy file sharing from the command line +type: application +version: 1.0.0 +appVersion: "latest" +home: https://github.com/dutchcoders/transfer.sh +sources: + - https://github.com/dutchcoders/transfer.sh +keywords: + - transfer + - file-sharing + - upload + - s3 + - storj + - gdrive diff --git a/k8s/transfer.sh/README.md b/k8s/transfer.sh/README.md new file mode 100644 index 000000000..9444dbf3b --- /dev/null +++ b/k8s/transfer.sh/README.md @@ -0,0 +1,334 @@ +# transfer.sh Helm Chart + +A Helm chart to deploy [transfer.sh](https://github.com/dutchcoders/transfer.sh) on Kubernetes — easy and secure file sharing from the command line. + +## Quick Start + +```bash +helm install transfer-sh ./transfer.sh +``` + +Upload a file: + +```bash +curl -u user:pass --upload-file ./file.txt https://transfer.example.com/file.txt +``` + +## Features + +- **Multiple storage backends**: local, S3 (including MinIO), Storj, Google Drive +- **Upload protection**: HTTP Basic Auth, htpasswd (multi-user), IP whitelist +- **Network security**: IP whitelist/blacklist, rate limiting +- **Virus scanning**: optional ClamAV integration +- **Ingress**: standard Kubernetes Ingress and Gateway API (HTTPRoute) support +- **Auto-purge**: automatic file deletion after configurable retention period + +## Storage Providers + +### Local (default) + +Files are stored on a PersistentVolumeClaim. + +```yaml +transfersh: + provider: "local" + local: + basedir: "/data" + +persistence: + enabled: true + size: 10Gi +``` + +### S3 / MinIO + +> **Note:** Tested with MinIO. Not tested directly with AWS S3. + +Compatible with AWS S3 and any S3-compatible storage (MinIO, Ceph, etc.). + +**Using a Kubernetes Secret (recommended):** + +```bash +kubectl create secret generic s3-creds \ + --from-literal=AWS_ACCESS_KEY= \ + --from-literal=AWS_SECRET_KEY= \ + -n +``` + +```yaml +transfersh: + provider: "s3" + s3: + bucket: "transfer" + region: "eu-west-1" + existingSecret: "s3-creds" + # For S3-compatible storage (MinIO, etc.): + # endpoint: "https://minio.example.com" + # pathStyle: true + +persistence: + enabled: false +``` + +**Using plain values (for testing only):** + +```yaml +transfersh: + provider: "s3" + s3: + bucket: "transfer" + region: "eu-west-1" + accessKey: "minioadmin" + secretKey: "minioadmin" + endpoint: "https://minio.example.com" + pathStyle: true + +persistence: + enabled: false +``` + +### Storj + +> **Note:** Not tested. + +**Using a Kubernetes Secret (recommended):** + +```bash +kubectl create secret generic storj-creds \ + --from-literal=STORJ_ACCESS= \ + -n +``` + +```yaml +transfersh: + provider: "storj" + storj: + bucket: "transfer" + existingSecret: "storj-creds" +``` + +**Using plain values (for testing only):** + +```yaml +transfersh: + provider: "storj" + storj: + bucket: "transfer" + access: "" +``` + +### Google Drive + +> **Note:** Not tested. + +Google Drive requires an OAuth `client.json` file obtained from the [Google Cloud Console](https://console.cloud.google.com/) (free, no billing required): enable the **Google Drive API**, then create an **OAuth client ID** (Desktop app) and download the JSON file. + +**Using a Kubernetes Secret (recommended):** + +```bash +kubectl create secret generic gdrive-client-json \ + --from-file=client.json=/path/to/your/client.json \ + -n +``` + +```yaml +transfersh: + provider: "gdrive" + gdrive: + basedir: "/data" + existingSecret: "gdrive-client-json" + +persistence: + enabled: true + size: 10Gi +``` + +When `existingSecret` is set, the chart automatically mounts the `client.json` file at the path defined by `clientJsonFilepath` (default: `/config/gdrive/client.json`). + +**Using plain values (for testing only):** + +```yaml +transfersh: + provider: "gdrive" + gdrive: + basedir: "/data" + clientJsonFilepath: "/config/gdrive/client.json" + localConfigPath: "/config/gdrive" + +persistence: + enabled: true + size: 10Gi +``` + +In this case, you must manually mount the `client.json` file into the pod (e.g. via an extra volume or by placing it in the PVC). + +#### Initial OAuth authentication + +Google Drive requires a one-time OAuth consent flow via a browser. This cannot be done inside a Kubernetes pod directly. The recommended approach is to run transfer.sh locally first to complete the authorization: + +```bash +docker run -p 8080:8080 \ + -v /path/to/client.json:/config/gdrive/client.json \ + -v /path/to/gdrive-config:/config/gdrive \ + dutchcoders/transfer.sh \ + --provider gdrive \ + --basedir /data \ + --gdrive-client-json-filepath /config/gdrive/client.json \ + --gdrive-local-config-path /config/gdrive +``` + +Follow the URL printed in the logs, authorize access, then store the generated token in the PVC or as an additional Secret in your cluster. + +## Security + +### Upload Authentication + +Protects uploads only — downloads remain accessible via the generated link. + +```yaml +transfersh: + httpAuth: + enabled: true + user: "admin" + pass: "changeme" + # Or use an existing Secret: + # existingSecret: "transfer-auth" # must contain HTTP_AUTH_USER + HTTP_AUTH_PASS + # Or use htpasswd for multi-user: + # htpasswd: "/etc/htpasswd" +``` + +### Network Access Control + +```yaml +transfersh: + security: + rateLimit: 60 # requests per minute (0 = unlimited) + ipWhitelist: "10.0.0.0/8" # only these IPs can access (empty = all) + ipBlacklist: "1.2.3.4" # block specific IPs +``` + +### Kubernetes NetworkPolicy + +Restrict ingress traffic to transfer.sh pods at the Kubernetes network level. When enabled, all inbound connections are denied by default except those explicitly allowed. + +**Disabled by default** — enabling without configuration allows all ingress (safe, no disruption): + +```yaml +networkPolicy: + enabled: true +``` + +**Restrict to your ingress controller namespace** (recommended): + +```yaml +networkPolicy: + enabled: true + ingressNamespace: "traefik" # or "ingress-nginx", "kube-system", etc. +``` + +Only pods from the specified namespace can reach transfer.sh on port 8080. All other namespaces are blocked. + +**Custom rules** — allow additional traffic (e.g. monitoring scrapes): + +```yaml +networkPolicy: + enabled: true + ingressNamespace: "traefik" + additionalIngressRules: + - from: + - podSelector: + matchLabels: + app: prometheus + ports: + - port: 8080 +``` + +> **Note:** Egress (outbound) traffic is not restricted by this NetworkPolicy. transfer.sh can still reach external storage backends (S3, Storj, etc.). + +> **Note:** Log collection via Promtail/Fluent Bit is not affected — those tools read logs from the node filesystem, not via network connections to the pod. + +### ClamAV Virus Scanning + +```yaml +transfersh: + clamav: + host: "clamav.clamav.svc.cluster.local:3310" # ..svc.cluster.local:PORT + prescan: false +``` + +## Ingress + +### Standard Kubernetes Ingress + +```yaml +ingress: + enabled: true + className: "nginx" + hosts: + - host: transfer.example.com + paths: + - path: / + pathType: Prefix + tls: + - secretName: transfer-tls + hosts: + - transfer.example.com +``` + +### Gateway API (HTTPRoute) + +```yaml +gatewayApi: + enabled: true + parentRefs: + - name: my-gateway + namespace: default + hostnames: + - transfer.example.com +``` + +## Values Reference + +| Key | Default | Description | +|-----|---------|-------------| +| `replicaCount` | `1` | Number of replicas | +| `image.repository` | `dutchcoders/transfer.sh` | Container image repository | +| `image.tag` | `latest` | Container image tag | +| `transfersh.provider` | `local` | Storage backend: `local`, `s3`, `storj`, `gdrive` | +| `transfersh.purgeDays` | `7` | Auto-delete files after N days (0 = disabled) | +| `transfersh.purgeInterval` | `1` | Purge check interval in hours | +| `transfersh.maxUploadSize` | `3145728` | Max upload size in KB (0 = unlimited) | +| `transfersh.randomTokenLength` | `6` | Length of the random token in file URLs | +| `transfersh.local.basedir` | `/data` | Base directory for local storage | +| `transfersh.s3.bucket` | `""` | S3 bucket name | +| `transfersh.s3.region` | `eu-west-1` | S3 region | +| `transfersh.s3.endpoint` | `""` | Custom S3 endpoint (MinIO, etc.) | +| `transfersh.s3.pathStyle` | `false` | Force path-style URLs (required for MinIO) | +| `transfersh.s3.noMultipart` | `false` | Disable multipart uploads | +| `transfersh.s3.existingSecret` | `""` | Secret with `AWS_ACCESS_KEY` + `AWS_SECRET_KEY` | +| `transfersh.storj.bucket` | `""` | Storj bucket name | +| `transfersh.storj.existingSecret` | `""` | Secret with `STORJ_ACCESS` key | +| `transfersh.gdrive.basedir` | `/data` | Base directory for Google Drive storage | +| `transfersh.gdrive.clientJsonFilepath` | `/config/gdrive/client.json` | Mount path for the OAuth client JSON file | +| `transfersh.gdrive.localConfigPath` | `/config/gdrive` | Local config/cache directory for gdrive tokens | +| `transfersh.gdrive.existingSecret` | `""` | Secret containing the `client.json` key (auto-mounted) | +| `transfersh.httpAuth.enabled` | `false` | Enable HTTP Basic Auth on uploads | +| `transfersh.httpAuth.existingSecret` | `""` | Secret with `HTTP_AUTH_USER` + `HTTP_AUTH_PASS` | +| `transfersh.httpAuth.user` | `""` | Basic auth username | +| `transfersh.httpAuth.pass` | `""` | Basic auth password | +| `transfersh.httpAuth.htpasswd` | `""` | htpasswd file path (takes precedence over user/pass) | +| `transfersh.httpAuth.ipWhitelist` | `""` | IPs allowed to upload without auth | +| `transfersh.security.rateLimit` | `30` | Rate limit in requests/min (0 = unlimited) | +| `transfersh.security.ipWhitelist` | `""` | Allowed IPs (comma-separated) | +| `transfersh.security.ipBlacklist` | `""` | Blocked IPs (comma-separated) | +| `transfersh.clamav.host` | `""` | ClamAV daemon address | +| `transfersh.clamav.prescan` | `false` | Enable ClamAV prescan | +| `transfersh.extraEnv` | `{}` | Extra environment variables | +| `persistence.enabled` | `false` | Enable PVC for local/gdrive storage | +| `persistence.size` | `10Gi` | PVC size | +| `persistence.accessMode` | `ReadWriteOnce` | PVC access mode | +| `ingress.enabled` | `false` | Enable Kubernetes Ingress | +| `gatewayApi.enabled` | `false` | Enable Gateway API HTTPRoute | +| `networkPolicy.enabled` | `false` | Enable Kubernetes NetworkPolicy to restrict ingress traffic | +| `networkPolicy.ingressNamespace` | `""` | Namespace of the ingress controller allowed to reach the app (empty = allow all) | +| `networkPolicy.additionalIngressRules` | `[]` | Additional raw NetworkPolicy ingress rules | diff --git a/k8s/transfer.sh/templates/NOTES.txt b/k8s/transfer.sh/templates/NOTES.txt new file mode 100644 index 000000000..61b23380c --- /dev/null +++ b/k8s/transfer.sh/templates/NOTES.txt @@ -0,0 +1,42 @@ +transfer.sh has been deployed successfully! + +Storage provider: {{ .Values.transfersh.provider }} +{{- if eq .Values.transfersh.provider "local" }} + Data directory: {{ .Values.transfersh.local.basedir }} + Persistence: {{ if .Values.persistence.enabled }}enabled ({{ .Values.persistence.size }}){{ else }}disabled (data will be lost on restart){{ end }} +{{- else if eq .Values.transfersh.provider "s3" }} + Bucket: {{ .Values.transfersh.s3.bucket }} + Region: {{ .Values.transfersh.s3.region }} + {{- if .Values.transfersh.s3.endpoint }} + Endpoint: {{ .Values.transfersh.s3.endpoint }} (S3-compatible) + {{- end }} +{{- else if eq .Values.transfersh.provider "storj" }} + Bucket: {{ .Values.transfersh.storj.bucket }} +{{- else if eq .Values.transfersh.provider "gdrive" }} + Data directory: {{ .Values.transfersh.gdrive.basedir }} +{{- end }} + +{{- if .Values.transfersh.httpAuth.enabled }} + +Upload authentication: enabled + Uploads require HTTP Basic Auth credentials. +{{- else }} + +Upload authentication: disabled + Anyone with access can upload files. Consider enabling httpAuth. +{{- end }} + +{{- if .Values.transfersh.purgeDays }} +File retention: {{ .Values.transfersh.purgeDays }} days +{{- end }} + +{{- if .Values.transfersh.maxUploadSize }} +Max upload size: {{ int .Values.transfersh.maxUploadSize }} KB +{{- end }} + +{{- if .Values.transfersh.security.rateLimit }} +Rate limit: {{ .Values.transfersh.security.rateLimit }} requests/min +{{- end }} + +Upload a file: + curl {{ if .Values.transfersh.httpAuth.enabled }}-u user:pass {{ end }}--upload-file ./file.txt https:///file.txt diff --git a/k8s/transfer.sh/templates/_helpers.tpl b/k8s/transfer.sh/templates/_helpers.tpl new file mode 100644 index 000000000..7042f9cff --- /dev/null +++ b/k8s/transfer.sh/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "transfer.sh.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "transfer.sh.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "transfer.sh.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "transfer.sh.labels" -}} +helm.sh/chart: {{ include "transfer.sh.chart" . }} +{{ include "transfer.sh.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "transfer.sh.selectorLabels" -}} +app.kubernetes.io/name: {{ include "transfer.sh.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "transfer.sh.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "transfer.sh.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/k8s/transfer.sh/templates/configmap.yaml b/k8s/transfer.sh/templates/configmap.yaml new file mode 100644 index 000000000..1d2cf76ff --- /dev/null +++ b/k8s/transfer.sh/templates/configmap.yaml @@ -0,0 +1,100 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +data: + PROVIDER: {{ .Values.transfersh.provider | quote }} + {{- if .Values.transfersh.purgeDays }} + PURGE_DAYS: {{ .Values.transfersh.purgeDays | quote }} + {{- end }} + {{- if .Values.transfersh.purgeInterval }} + PURGE_INTERVAL: {{ .Values.transfersh.purgeInterval | quote }} + {{- end }} + {{- if .Values.transfersh.maxUploadSize }} + MAX_UPLOAD_SIZE: {{ int .Values.transfersh.maxUploadSize | quote }} + {{- end }} + {{- if .Values.transfersh.randomTokenLength }} + RANDOM_TOKEN_LENGTH: {{ .Values.transfersh.randomTokenLength | quote }} + {{- end }} + {{- /* Local provider */}} + {{- if eq .Values.transfersh.provider "local" }} + BASEDIR: {{ .Values.transfersh.local.basedir | quote }} + {{- end }} + {{- /* S3 provider */}} + {{- if eq .Values.transfersh.provider "s3" }} + {{- with .Values.transfersh.s3 }} + BUCKET: {{ .bucket | quote }} + S3_REGION: {{ .region | quote }} + {{- if .endpoint }} + S3_ENDPOINT: {{ .endpoint | quote }} + {{- end }} + {{- if .pathStyle }} + S3_PATH_STYLE: "true" + {{- end }} + {{- if .noMultipart }} + S3_NO_MULTIPART: "true" + {{- end }} + {{- if and (not .existingSecret) .accessKey }} + AWS_ACCESS_KEY: {{ .accessKey | quote }} + {{- end }} + {{- if and (not .existingSecret) .secretKey }} + AWS_SECRET_KEY: {{ .secretKey | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- /* Storj provider */}} + {{- if eq .Values.transfersh.provider "storj" }} + {{- with .Values.transfersh.storj }} + STORJ_BUCKET: {{ .bucket | quote }} + {{- if and (not .existingSecret) .access }} + STORJ_ACCESS: {{ .access | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- /* Google Drive provider */}} + {{- if eq .Values.transfersh.provider "gdrive" }} + {{- with .Values.transfersh.gdrive }} + BASEDIR: {{ .basedir | quote }} + {{- if .clientJsonFilepath }} + GDRIVE_CLIENT_JSON_FILEPATH: {{ .clientJsonFilepath | quote }} + {{- end }} + {{- if .localConfigPath }} + GDRIVE_LOCAL_CONFIG_PATH: {{ .localConfigPath | quote }} + {{- end }} + {{- end }} + {{- end }} + {{- /* HTTP Authentication */}} + {{- if .Values.transfersh.httpAuth.enabled }} + {{- if .Values.transfersh.httpAuth.htpasswd }} + HTTP_AUTH_HTPASSWD: {{ .Values.transfersh.httpAuth.htpasswd | quote }} + {{- else if not .Values.transfersh.httpAuth.existingSecret }} + HTTP_AUTH_USER: {{ .Values.transfersh.httpAuth.user | quote }} + HTTP_AUTH_PASS: {{ .Values.transfersh.httpAuth.pass | quote }} + {{- end }} + {{- if .Values.transfersh.httpAuth.ipWhitelist }} + HTTP_AUTH_IP_WHITELIST: {{ .Values.transfersh.httpAuth.ipWhitelist | quote }} + {{- end }} + {{- end }} + {{- /* Network access control */}} + {{- if .Values.transfersh.security.ipWhitelist }} + IP_WHITELIST: {{ .Values.transfersh.security.ipWhitelist | quote }} + {{- end }} + {{- if .Values.transfersh.security.ipBlacklist }} + IP_BLACKLIST: {{ .Values.transfersh.security.ipBlacklist | quote }} + {{- end }} + {{- if .Values.transfersh.security.rateLimit }} + RATE_LIMIT: {{ .Values.transfersh.security.rateLimit | quote }} + {{- end }} + {{- /* ClamAV */}} + {{- if .Values.transfersh.clamav.host }} + CLAMAV_HOST: {{ .Values.transfersh.clamav.host | quote }} + {{- if .Values.transfersh.clamav.prescan }} + PERFORM_CLAMAV_PRESCAN: "true" + {{- end }} + {{- end }} + {{- /* Extra env vars */}} + {{- range $key, $value := .Values.transfersh.extraEnv }} + {{ $key }}: {{ $value | quote }} + {{- end }} diff --git a/k8s/transfer.sh/templates/deployment.yaml b/k8s/transfer.sh/templates/deployment.yaml new file mode 100644 index 000000000..7f9812bf3 --- /dev/null +++ b/k8s/transfer.sh/templates/deployment.yaml @@ -0,0 +1,130 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + {{- with .Values.strategy }} + strategy: + {{- toYaml . | nindent 4 }} + {{- end }} + selector: + matchLabels: + {{- include "transfer.sh.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "transfer.sh.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "transfer.sh.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "transfer.sh.fullname" . }} + {{- if or (and (eq .Values.transfersh.provider "s3") .Values.transfersh.s3.existingSecret) (and (eq .Values.transfersh.provider "storj") .Values.transfersh.storj.existingSecret) (and .Values.transfersh.httpAuth.enabled .Values.transfersh.httpAuth.existingSecret) }} + env: + {{- /* S3 secret credentials */}} + {{- if and (eq .Values.transfersh.provider "s3") .Values.transfersh.s3.existingSecret }} + - name: AWS_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.transfersh.s3.existingSecret }} + key: AWS_ACCESS_KEY + - name: AWS_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.transfersh.s3.existingSecret }} + key: AWS_SECRET_KEY + {{- end }} + {{- /* Storj secret credentials */}} + {{- if and (eq .Values.transfersh.provider "storj") .Values.transfersh.storj.existingSecret }} + - name: STORJ_ACCESS + valueFrom: + secretKeyRef: + name: {{ .Values.transfersh.storj.existingSecret }} + key: STORJ_ACCESS + {{- end }} + {{- /* HTTP Auth secret credentials */}} + {{- if and .Values.transfersh.httpAuth.enabled .Values.transfersh.httpAuth.existingSecret }} + - name: HTTP_AUTH_USER + valueFrom: + secretKeyRef: + name: {{ .Values.transfersh.httpAuth.existingSecret }} + key: HTTP_AUTH_USER + - name: HTTP_AUTH_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.transfersh.httpAuth.existingSecret }} + key: HTTP_AUTH_PASS + {{- end }} + {{- end }} + livenessProbe: + {{- toYaml .Values.livenessProbe | nindent 12 }} + readinessProbe: + {{- toYaml .Values.readinessProbe | nindent 12 }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: tmp + mountPath: /tmp + - name: data + mountPath: {{ ternary .Values.transfersh.gdrive.basedir .Values.transfersh.local.basedir (eq .Values.transfersh.provider "gdrive") }} + {{- if and (eq .Values.transfersh.provider "gdrive") .Values.transfersh.gdrive.existingSecret }} + - name: gdrive-client-json + mountPath: {{ .Values.transfersh.gdrive.clientJsonFilepath }} + subPath: client.json + readOnly: true + {{- end }} + volumes: + - name: tmp + emptyDir: {} + - name: data + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ include "transfer.sh.fullname" . }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if and (eq .Values.transfersh.provider "gdrive") .Values.transfersh.gdrive.existingSecret }} + - name: gdrive-client-json + secret: + secretName: {{ .Values.transfersh.gdrive.existingSecret }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/k8s/transfer.sh/templates/hpa.yaml b/k8s/transfer.sh/templates/hpa.yaml new file mode 100644 index 000000000..c686126b6 --- /dev/null +++ b/k8s/transfer.sh/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "transfer.sh.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/k8s/transfer.sh/templates/httproute.yaml b/k8s/transfer.sh/templates/httproute.yaml new file mode 100644 index 000000000..92cf216f4 --- /dev/null +++ b/k8s/transfer.sh/templates/httproute.yaml @@ -0,0 +1,37 @@ +{{- if .Values.gatewayApi.enabled -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} + {{- with .Values.gatewayApi.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- range .Values.gatewayApi.parentRefs }} + - name: {{ .name }} + {{- if .namespace }} + namespace: {{ .namespace }} + {{- end }} + {{- if .sectionName }} + sectionName: {{ .sectionName }} + {{- end }} + {{- end }} + {{- with .Values.gatewayApi.hostnames }} + hostnames: + {{- range . }} + - {{ . | quote }} + {{- end }} + {{- end }} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: {{ include "transfer.sh.fullname" . }} + port: {{ .Values.service.port }} +{{- end }} diff --git a/k8s/transfer.sh/templates/ingress.yaml b/k8s/transfer.sh/templates/ingress.yaml new file mode 100644 index 000000000..659e767a1 --- /dev/null +++ b/k8s/transfer.sh/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "transfer.sh.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/k8s/transfer.sh/templates/networkpolicy.yaml b/k8s/transfer.sh/templates/networkpolicy.yaml new file mode 100644 index 000000000..d7ad45965 --- /dev/null +++ b/k8s/transfer.sh/templates/networkpolicy.yaml @@ -0,0 +1,29 @@ +{{- if .Values.networkPolicy.enabled }} +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +spec: + podSelector: + matchLabels: + {{- include "transfer.sh.selectorLabels" . | nindent 6 }} + policyTypes: + - Ingress + ingress: + {{- if .Values.networkPolicy.ingressNamespace }} + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Values.networkPolicy.ingressNamespace }} + ports: + - port: 8080 + protocol: TCP + {{- else }} + - {} + {{- end }} + {{- with .Values.networkPolicy.additionalIngressRules }} + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/k8s/transfer.sh/templates/pvc.yaml b/k8s/transfer.sh/templates/pvc.yaml new file mode 100644 index 000000000..b3c9c5aa9 --- /dev/null +++ b/k8s/transfer.sh/templates/pvc.yaml @@ -0,0 +1,17 @@ +{{- if .Values.persistence.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +spec: + accessModes: + - {{ .Values.persistence.accessMode }} + resources: + requests: + storage: {{ .Values.persistence.size }} + {{- if .Values.persistence.storageClass }} + storageClassName: {{ .Values.persistence.storageClass }} + {{- end }} +{{- end }} diff --git a/k8s/transfer.sh/templates/service.yaml b/k8s/transfer.sh/templates/service.yaml new file mode 100644 index 000000000..3e4ebd9e5 --- /dev/null +++ b/k8s/transfer.sh/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "transfer.sh.fullname" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "transfer.sh.selectorLabels" . | nindent 4 }} diff --git a/k8s/transfer.sh/templates/serviceaccount.yaml b/k8s/transfer.sh/templates/serviceaccount.yaml new file mode 100644 index 000000000..6728ff351 --- /dev/null +++ b/k8s/transfer.sh/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "transfer.sh.serviceAccountName" . }} + labels: + {{- include "transfer.sh.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/k8s/transfer.sh/values.yaml b/k8s/transfer.sh/values.yaml new file mode 100644 index 000000000..ba2f1abf9 --- /dev/null +++ b/k8s/transfer.sh/values.yaml @@ -0,0 +1,222 @@ +# Default values for transfer.sh. + +replicaCount: 1 + +strategy: {} + +image: + repository: dutchcoders/transfer.sh + pullPolicy: IfNotPresent + tag: "latest-noroot" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "transfersh" + +# transfer.sh configuration +# See https://github.com/dutchcoders/transfer.sh for full list +transfersh: + # Storage provider: local, s3, storj, gdrive + provider: "local" + + # -- General settings + # Auto-delete uploads after N days (0 = disabled) + purgeDays: 7 + # Purge check interval in hours + purgeInterval: 1 + # Max upload size in kilobytes (0 = unlimited) + maxUploadSize: 3145728 + # Random token length for file URLs + randomTokenLength: 6 + + # -- Local storage settings (provider: local) + local: + # Base directory for local storage + basedir: "/data" + + # -- S3 storage settings (provider: s3) + s3: + bucket: "" + # Choose what region your provider is + region: "eu-west-1" + # Custom endpoint for S3-compatible storage (MinIO, etc.) + endpoint: "" + # Force path-style URLs (required for MinIO) + pathStyle: false + # Set to true to disable multipart uploads + noMultipart: false + # Use an existing secret for AWS credentials + # Secret must contain AWS_ACCESS_KEY and AWS_SECRET_KEY keys + existingSecret: "" + # Or set credentials directly (not recommended for production) + accessKey: "" + secretKey: "" + + # -- Storj storage settings (provider: storj) + storj: + bucket: "" + # Use an existing secret containing STORJ_ACCESS key + existingSecret: "" + # Or set access grant directly + access: "" + + # -- Google Drive storage settings (provider: gdrive) + gdrive: + # Base directory for gdrive storage + basedir: "" + # Path to the OAuth client credentials JSON file (auto-set when using existingSecret) + clientJsonFilepath: "/config/gdrive/client.json" + # Local config cache directory + localConfigPath: "/config/gdrive" + # Use an existing secret containing the OAuth client.json file + # Secret must contain a key named "client.json" + existingSecret: "" + + # -- Authentication (protects uploads only, downloads remain accessible via link) + httpAuth: + enabled: false + # Use an existing secret containing HTTP_AUTH_USER and HTTP_AUTH_PASS keys + existingSecret: "" + # Or set credentials directly + user: "" + pass: "" + # Htpasswd file path for multi-user basic auth on upload + # If set, takes precedence over user/pass + htpasswd: "" + # Comma-separated list of IPs allowed to upload without auth challenge + ipWhitelist: "" + + # -- Network access control + security: + # Comma-separated list of IPs allowed to connect (upload AND download) + # Leave empty to allow all + ipWhitelist: "" + # Comma-separated list of IPs blocked from connecting + ipBlacklist: "" + # Rate limit in requests per minute (0 = unlimited) + rateLimit: 30 + + # -- Virus scanning with ClamAV + clamav: + # ClamAV daemon host (e.g. clamav.clamav.svc.cluster.local:3310) + host: "" + # Prescan uploads using local clamd unix socket + prescan: false + + # -- Extra environment variables for any additional configuration + extraEnv: {} + # CORS_DOMAINS: "*" + # TEMP_PATH: "/tmp" + # LOG: "/dev/stdout" + +# Persistent storage (used for local and gdrive providers) +persistence: + enabled: false + accessMode: ReadWriteOnce + size: 10Gi + # storageClass: "" + +serviceAccount: + create: true + automount: false + annotations: {} + name: "" + +podAnnotations: {} +podLabels: {} + +podSecurityContext: + fsGroup: 5000 + seccompProfile: + type: RuntimeDefault + +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 5000 + capabilities: + drop: + - ALL + +service: + type: ClusterIP + port: 8080 + +ingress: + enabled: false + className: "" + annotations: {} + # cert-manager.io/cluster-issuer: my-issuers + hosts: + - host: transfer.example.com + paths: + - path: / + pathType: Prefix + tls: [] + # - secretName: transfer-sh-tls + # hosts: + # - transfer.example.com + +# Gateway API HTTPRoute configuration +# Requires a Gateway resource to be deployed separately +gatewayApi: + enabled: false + annotations: {} + # Reference to the parent Gateway(s) + parentRefs: + - name: my-gateway + # namespace: default + # sectionName: https + # Hostnames this route should match + hostnames: + - transfer.example.com + +resources: + requests: + memory: 128Mi + limits: + memory: 384Mi + + +livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + +readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + +networkPolicy: + # Enable Kubernetes NetworkPolicy to restrict ingress traffic to transfer.sh pods. + # When disabled, no NetworkPolicy is created and all traffic is allowed (default Kubernetes behaviour). + enabled: false + # Namespace of your ingress controller (e.g. "traefik", "ingress-nginx", "kube-system"). + # When set, only pods from this namespace are allowed to reach transfer.sh. + # When empty, ingress from all namespaces remains allowed. + ingressNamespace: "" + # Additional raw NetworkPolicy ingress rules. + # Useful for allowing specific traffic (monitoring scrapes, health checks, etc.). + additionalIngressRules: [] + # - from: + # - podSelector: + # matchLabels: + # app: prometheus + # ports: + # - port: 8080 + +nodeSelector: {} + +tolerations: [] + +affinity: {} From da4ab65503402cfd23763dbe41cbf3233ac528f4 Mon Sep 17 00:00:00 2001 From: Adrien Date: Wed, 18 Feb 2026 13:03:03 +0100 Subject: [PATCH 2/2] Update readme.md with noroot image as default one --- k8s/transfer.sh/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/transfer.sh/README.md b/k8s/transfer.sh/README.md index 9444dbf3b..e41702316 100644 --- a/k8s/transfer.sh/README.md +++ b/k8s/transfer.sh/README.md @@ -293,7 +293,7 @@ gatewayApi: |-----|---------|-------------| | `replicaCount` | `1` | Number of replicas | | `image.repository` | `dutchcoders/transfer.sh` | Container image repository | -| `image.tag` | `latest` | Container image tag | +| `image.tag` | `latest-noroot` | Container image tag - chart is optimize for non root env | | `transfersh.provider` | `local` | Storage backend: `local`, `s3`, `storj`, `gdrive` | | `transfersh.purgeDays` | `7` | Auto-delete files after N days (0 = disabled) | | `transfersh.purgeInterval` | `1` | Purge check interval in hours |