{"id":49254699,"url":"https://github.com/soakes/s3ctl","last_synced_at":"2026-05-01T03:02:16.123Z","repository":{"id":353639527,"uuid":"1220107429","full_name":"soakes/s3ctl","owner":"soakes","description":"Go-based S3 provisioning CLI for bucket creation, scoped IAM credentials, and batch operations.","archived":false,"fork":false,"pushed_at":"2026-04-25T01:26:42.000Z","size":139,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T02:16:47.225Z","etag":null,"topics":["automation","cli","devops","golang","iam","object-storage","s3"],"latest_commit_sha":null,"homepage":"https://soakes.github.io/s3ctl/","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/soakes.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":"AGENTS.md","dco":null,"cla":null}},"created_at":"2026-04-24T14:53:21.000Z","updated_at":"2026-04-25T01:26:46.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/soakes/s3ctl","commit_stats":null,"previous_names":["soakes/s3ctl"],"tags_count":20,"template":false,"template_full_name":null,"purl":"pkg:github/soakes/s3ctl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soakes%2Fs3ctl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soakes%2Fs3ctl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soakes%2Fs3ctl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soakes%2Fs3ctl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soakes","download_url":"https://codeload.github.com/soakes/s3ctl/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soakes%2Fs3ctl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32483406,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-30T13:12:12.517Z","status":"online","status_checked_at":"2026-05-01T02:00:05.856Z","response_time":64,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automation","cli","devops","golang","iam","object-storage","s3"],"created_at":"2026-04-25T02:12:38.893Z","updated_at":"2026-05-01T03:02:16.110Z","avatar_url":"https://github.com/soakes.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🪣 s3ctl\n\n\u003e A single-binary CLI for creating S3-compatible buckets and issuing bucket-scoped credentials.\n\n[![Validate](https://img.shields.io/github/actions/workflow/status/soakes/s3ctl/build-and-validate.yml?branch=main\u0026style=flat-square\u0026label=validate)](https://github.com/soakes/s3ctl/actions/workflows/build-and-validate.yml)\n[![Container](https://img.shields.io/github/actions/workflow/status/soakes/s3ctl/container-image.yml?branch=main\u0026style=flat-square\u0026label=container)](https://github.com/soakes/s3ctl/actions/workflows/container-image.yml)\n[![Release](https://img.shields.io/github/v/release/soakes/s3ctl?sort=semver\u0026style=flat-square)](https://github.com/soakes/s3ctl/releases)\n[![APT Repository](https://img.shields.io/badge/APT-signed%20repo-A81D33?style=flat-square\u0026logo=debian\u0026logoColor=white)](https://soakes.github.io/s3ctl/)\n[![GHCR](https://img.shields.io/badge/GHCR-published-2088FF?style=flat-square\u0026logo=github)](https://ghcr.io/soakes/s3ctl)\n[![Go](https://img.shields.io/badge/Go-1.26.2-00ADD8.svg?style=flat-square\u0026logo=go\u0026logoColor=white)](https://go.dev/)\n\n`s3ctl` is for teams that need repeatable bucket provisioning without manual\nstorage and IAM setup. It creates buckets, issues scoped credentials, rotates\nOVHcloud keys, deletes empty buckets safely, and is available as release\narchives, Debian packages, an APT package, and a container image.\n\n**Links:** [📦 Releases](https://github.com/soakes/s3ctl/releases) · [🐳 GHCR](https://ghcr.io/soakes/s3ctl) · [🔐 Release Hub / APT](https://soakes.github.io/s3ctl/) · [🧰 Examples](examples)\n\n## 🧭 Table of Contents\n\n- [📖 Overview](#overview)\n- [✨ Capabilities](#capabilities)\n- [🚀 Quick Start](#quick-start)\n- [📦 Distribution](#distribution)\n- [🖥️ Website Preview](#website-preview)\n- [🗃️ Batch Provisioning](#batch-provisioning)\n- [⚙️ Configuration](#configuration)\n- [🧩 Built-In Templates](#built-in-templates)\n- [🔑 IAM Notes](#iam-notes)\n- [🧹 Deleting Buckets](#deleting-buckets)\n- [☁️ OVHcloud Notes](#ovhcloud-notes)\n- [🐳 Container](#container)\n- [🛠️ Development](#development)\n- [🚢 Release Process](#release-process)\n\n---\n\n\u003ca id=\"overview\"\u003e\u003c/a\u003e\n\n## 📖 Overview\n\n`s3ctl` provisions S3-compatible buckets and automatically issues\nbucket-scoped access credentials. It can work with a normal S3/IAM-compatible\nprovider, or with OVHcloud Public Cloud Object Storage where buckets are exposed\nas S3-compatible containers.\n\nIt is designed for the common operational workflow:\n\n- create one or many buckets\n- optionally enable versioning\n- optionally apply a bucket policy from a built-in template or JSON file\n- create a fresh access key and secret key for each bucket\n- attach a generated policy so each credential only has access to its own bucket\n- rotate existing OVHcloud S3 credentials by bucket name\n- delete empty buckets safely, or delete non-empty buckets with an explicit force guard\n- drive the same workflow from flags, JSON config, or CSV batch input\n\n```mermaid\nflowchart LR\n  input[\"Flags, JSON config, or CSV batch\"] --\u003e plan[\"s3ctl builds a per-bucket plan\"]\n  plan --\u003e provider{\"Provider\"}\n  provider --\u003e|s3| s3api[\"S3 API creates and configures buckets\"]\n  provider --\u003e|s3 with scoped credentials| iamapi[\"IAM API creates users, policies, and access keys\"]\n  provider --\u003e|ovh| ovhapi[\"OVHcloud API creates containers, users, policies, and S3 keys\"]\n  s3api --\u003e output[\"Text or JSON output\"]\n  iamapi --\u003e output\n  ovhapi --\u003e output\n  output --\u003e operator[\"Endpoint, region, and scoped credentials\"]\n```\n\n### ✅ First Bucket Checklist\n\n1. Put shared provider settings in `~/.config/s3ctl/config.json`.\n2. Run `s3ctl --bucket app-data --dry-run --output json`.\n3. Confirm the endpoint, region, and credential scope in the plan.\n4. Run `s3ctl --bucket app-data --output json`.\n5. Store the returned access key and secret securely; secrets are only printed once.\n\n---\n\n\u003ca id=\"capabilities\"\u003e\u003c/a\u003e\n\n## ✨ Capabilities\n\n- **Bucket provisioning**: creates one bucket, many buckets, or CSV-driven batches\n- **Scoped credentials**: creates bucket-specific IAM-style users and access keys\n- **OVHcloud support**: creates containers, Public Cloud users, S3 keys, policies, and optional encryption\n- **Credential rotation**: rotates OVHcloud S3 keypairs by bucket/user name\n- **OVHcloud policy repair**: reapplies scoped S3 user policies to existing bucket users\n- **Safe deletion**: deletes empty buckets without `--force` and requires `--force` for non-empty buckets\n- **JSON output**: emits success and error payloads for machine workflows\n- **Install options**: provides release archives, Debian packages, a signed APT repository, and GHCR images\n- **Validated releases**: publishes stable builds after release-candidate validation\n\n---\n\n\u003ca id=\"quick-start\"\u003e\u003c/a\u003e\n\n## 🚀 Quick Start\n\nBuild locally:\n\n```bash\nmake build\n./dist/s3ctl --help\n```\n\n`s3ctl --help` is a short operator quick reference. Use `s3ctl --help-full`\nwhen you need the complete flag, template, and batch CSV reference.\n\nInstall the latest published binary:\n\n```bash\ncurl -fsSL https://soakes.github.io/s3ctl/install.sh | bash\n```\n\nOn macOS, use the installer instead of manually unpacking the release archive.\nThe published macOS binaries are not Apple-notarized yet, so manually extracted\ndownloads may be blocked by Gatekeeper unless the quarantine marker is removed.\nThe installer handles that step after placing the binary in a user-owned bin\ndirectory.\n\nPlan a single bucket with generated scoped credentials:\n\n```bash\ns3ctl \\\n  --bucket app-data \\\n  --endpoint https://objects.example.com \\\n  --region us-east-1 \\\n  --create-scoped-credentials \\\n  --credential-policy-template bucket-readwrite \\\n  --dry-run\n```\n\nProvision an OVHcloud Object Storage container and a dedicated S3 key:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --region UK \\\n  --ovh-service-name PUBLIC_CLOUD_PROJECT_ID \\\n  --output json\n```\n\nRotate an existing OVHcloud bucket keypair:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --ovh-rotate-credentials \\\n  --output json\n```\n\nRepair OVHcloud bucket scoping for an existing bucket user:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --ovh-repair-policies \\\n  --output json\n```\n\nPreview a bucket delete:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --delete \\\n  --dry-run\n```\n\nDelete an empty bucket after checking the dry-run output:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --delete\n```\n\nDelete a non-empty bucket after checking the dry-run output:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket app-data \\\n  --delete \\\n  --force\n```\n\nShow focused bucket workflow help:\n\n```bash\ns3ctl --bucket app-data --help\n```\n\nShow the full CLI reference:\n\n```bash\ns3ctl --help-full\n```\n\nPlan multiple buckets from repeated flags:\n\n```bash\ns3ctl \\\n  --bucket app-data \\\n  --bucket logs-archive \\\n  --create-scoped-credentials \\\n  --dry-run \\\n  --output json\n```\n\nPlan a batch from CSV:\n\n```bash\ns3ctl \\\n  --batch-file ./examples/s3ctl-batch.csv \\\n  --endpoint https://objects.example.com \\\n  --region us-east-1 \\\n  --create-scoped-credentials \\\n  --dry-run \\\n  --output json\n```\n\n---\n\n\u003ca id=\"distribution\"\u003e\u003c/a\u003e\n\n## 📦 Distribution\n\nPublished artifacts cover the supported installation paths:\n\n- GitHub release archives for `linux/amd64`, `linux/arm64`, `linux/arm/v7`, `darwin/amd64`, and `darwin/arm64`\n- Debian `.deb` packages for `amd64`, `arm64`, and `armhf`\n- a GitHub Pages release hub with install commands and release metadata\n- a signed APT repository\n- a multi-arch GHCR image\n\nRelease candidates use tags like `v1.2.3-rc.1`. They are useful for testing a\nversion before it reaches the stable installer, stable APT channel, or\n`:latest` container tag.\n\nDirect installer, recommended for macOS:\n\n```bash\ncurl -fsSL https://soakes.github.io/s3ctl/install.sh | bash\n```\n\nOn macOS, install via this script unless you specifically need to handle the\narchive yourself. The installer defaults to a user-owned bin directory, prefers\nan existing home bin path already present in `PATH` such as `$HOME/.local/bin`,\n`$HOME/bin`, or `$HOME/.bin`, and otherwise uses `$HOME/.local/bin` with a PATH\nhint. It also clears the macOS download quarantine marker from the installed\nbinary.\n\nIf you download and extract a macOS archive manually, Finder may block the binary\nbecause the release is not Apple-notarized yet. Prefer the installer, or clear\nthe quarantine marker yourself after verifying the checksum:\n\n```bash\nxattr -d com.apple.quarantine ./s3ctl-darwin-arm64\n```\n\nPinned installer run:\n\n```bash\ncurl -fsSL https://soakes.github.io/s3ctl/install.sh | bash -s -- --version v1.2.3\n```\n\nCustom install location:\n\n```bash\ncurl -fsSL https://soakes.github.io/s3ctl/install.sh | bash -s -- --install-dir \"$HOME/.local/bin\"\n```\n\nDirect Debian package:\n\n```bash\ncurl -fsSLO https://github.com/soakes/s3ctl/releases/latest/download/s3ctl_1.2.3_amd64.deb\nsudo apt install ./s3ctl_1.2.3_amd64.deb\n```\n\nSigned APT repository:\n\n```bash\nsudo install -d -m 0755 /etc/apt/keyrings\ncurl -fsSL https://soakes.github.io/s3ctl/apt/s3ctl-archive-keyring.gpg \\\n  | sudo tee /etc/apt/keyrings/s3ctl-archive-keyring.gpg \u003e/dev/null\n\nsudo tee /etc/apt/sources.list.d/s3ctl.sources \u003e/dev/null \u003c\u003c'EOF'\nTypes: deb\nURIs: https://soakes.github.io/s3ctl/apt/\nSuites: stable\nComponents: main\nSigned-By: /etc/apt/keyrings/s3ctl-archive-keyring.gpg\nEOF\n\nsudo apt update \u0026\u0026 sudo apt install s3ctl\n```\n\n---\n\n\u003ca id=\"website-preview\"\u003e\u003c/a\u003e\n\n## 🖥️ Website Preview\n\nRender the release hub locally with real browser screenshots:\n\n```bash\nmake website-install\nmake website-check\nmake website-build\nmake website-capture\n```\n\nDesktop and mobile captures are written to `website/.captures/`.\nThe website is built with Vite and the local preview flow falls back to\n`website/preview-metadata.json` when generated release metadata is not present yet.\n\n---\n\n\u003ca id=\"batch-provisioning\"\u003e\u003c/a\u003e\n\n## 🗃️ Batch Provisioning\n\nFor bulk runs, the normal pattern is:\n\n1. Define the shared provider settings once with flags or config.\n2. Feed the bucket list in with repeated `--bucket` flags or `--batch-file`.\n3. Let `s3ctl` generate one scoped user and one access key pair per bucket.\n\nSupported batch CSV columns:\n\n- `bucket`\n- `iam_user_name`\n- `enable_versioning`\n- `bucket_policy_file`\n- `bucket_policy_template`\n- `create_scoped_credentials`\n- `credential_policy_template`\n\nExample CSV:\n\n```csv\nbucket,create_scoped_credentials,credential_policy_template,enable_versioning\napp-data,true,bucket-readwrite,true\nlogs-archive,true,bucket-readonly,false\n```\n\n---\n\n\u003ca id=\"configuration\"\u003e\u003c/a\u003e\n\n## ⚙️ Configuration\n\nConfiguration is resolved in this order:\n\n1. CLI flags\n2. JSON config file\n3. Built-in defaults\n\nExample config:\n\n```json\n{\n  \"endpoint\": \"https://objects.example.com\",\n  \"region\": \"us-east-1\",\n  \"enable_versioning\": true,\n  \"create_scoped_credentials\": true,\n  \"credential_policy_template\": \"bucket-readwrite\",\n  \"bucket_policy_template\": \"deny-insecure-transport\",\n  \"batch_file\": \"./s3ctl-batch.csv\"\n}\n```\n\nRun it:\n\n```bash\ns3ctl --config ./examples/s3ctl.json --dry-run --output json\n```\n\nWhen `--output json` or `\"output\": \"json\"` is set, command failures are also\nwritten to stdout as JSON. The process still exits non-zero, but automation can\nread the `error.code`, `error.message`, and\noptional `error.detail` fields instead of scraping text:\n\n```json\n{\n  \"operation\": \"delete\",\n  \"dry_run\": false,\n  \"config_file\": \"/home/operator/.config/s3ctl/config.json\",\n  \"resource_count\": 1,\n  \"error\": {\n    \"code\": \"not_found\",\n    \"message\": \"OVH bucket/container \\\"app-data\\\" does not exist in region \\\"UK\\\"; nothing was deleted\",\n    \"detail\": \"OVHcloud API error ...\"\n  }\n}\n```\n\nExample OVHcloud config with OAuth2 service account credentials:\n\n```json\n{\n  \"provider\": \"ovh\",\n  \"ovh_service_name\": \"PUBLIC_CLOUD_PROJECT_ID\",\n  \"ovh_client_id\": \"CLIENT_ID\",\n  \"ovh_client_secret\": \"CLIENT_SECRET\",\n  \"region\": \"UK\",\n  \"enable_versioning\": true,\n  \"ovh_encrypt_data\": true,\n  \"ovh_storage_policy_role\": \"readWrite\",\n  \"output\": \"json\"\n}\n```\n\nClassic OVH API application credentials are also supported:\n\n```json\n{\n  \"provider\": \"ovh\",\n  \"ovh_service_name\": \"PROJECT_ID\",\n  \"ovh_application_key\": \"APPLICATION_KEY\",\n  \"ovh_application_secret\": \"APPLICATION_SECRET\",\n  \"ovh_consumer_key\": \"CONSUMER_KEY\",\n  \"region\": \"GRA\"\n}\n```\n\nWith that saved in your default config, this is enough:\n\n```bash\ns3ctl --bucket app-data\n```\n\nRelative paths inside the config file are resolved from the config file directory, so config-local batch files and policy documents work cleanly.\n\nDefault user config path:\n\n- `$XDG_CONFIG_HOME/s3ctl/config.json`\n- `$HOME/.config/s3ctl/config.json`\n\nWhen `--config` is unset, `s3ctl` will automatically load that default file if\nit exists. This is the right place for shared operator settings such as\nprovider, endpoint, region, profile, credentials, IAM/OVH defaults, and output\npreferences.\n\nExample default user config:\n\n```json\n{\n  \"endpoint\": \"https://objects.example.com\",\n  \"region\": \"us-east-1\",\n  \"access_key\": \"MASTER_ACCESS_KEY_ID\",\n  \"secret_key\": \"MASTER_SECRET_ACCESS_KEY\",\n  \"create_scoped_credentials\": true,\n  \"credential_policy_template\": \"bucket-readwrite\"\n}\n```\n\nUse either `profile` or explicit `access_key` and `secret_key` values, not both.\nAdd `session_token` when your master credentials are temporary. If those values\nare not set in `s3ctl`, the AWS SDK still uses its normal credential and profile\ndiscovery. If you keep secrets in the default user config, store that file\noutside the repository and restrict its permissions.\n\nInstall that as your per-user default:\n\n```bash\ninstall -d -m 700 \"${XDG_CONFIG_HOME:-$HOME/.config}/s3ctl\"\ninstall -m 600 ./examples/user-config.json \"${XDG_CONFIG_HOME:-$HOME/.config}/s3ctl/config.json\"\n```\n\n---\n\n\u003ca id=\"built-in-templates\"\u003e\u003c/a\u003e\n\n## 🧩 Built-In Templates\n\nBucket policy templates:\n\n| Template | Coverage |\n| --- | --- |\n| `deny-insecure-transport` | Denies all S3 actions against the bucket and objects when requests do not use secure transport. |\n| `public-read` | Allows public `s3:GetObject` access to objects in the bucket. |\n\nScoped credential policy templates:\n\n| Template | Coverage |\n| --- | --- |\n| `bucket-readonly` | Allows bucket location lookup, bucket listing, and object reads for one bucket. |\n| `bucket-readwrite` | Allows bucket location lookup, bucket listing, object reads, writes, deletes, and multipart upload operations for one bucket. |\n| `bucket-admin` | Allows all S3 actions against one bucket and its objects. |\n\nBy default, generated scoped credentials use `bucket-readwrite`, generated IAM\nuser names are derived directly from bucket names, and no IAM path is set.\nConfigure `iam_user_prefix` or `--iam-user-prefix` when generated user names\nshould share a prefix. Configure `iam_path` or `--iam-path` when generated\nusers should be created under an IAM path.\n\n---\n\n\u003ca id=\"iam-notes\"\u003e\u003c/a\u003e\n\n## 🔑 IAM Notes\n\nScoped credential provisioning uses the IAM API in addition to the S3 API. The principal running `s3ctl` therefore needs permission to:\n\n- create buckets and apply bucket configuration in S3\n- create IAM users\n- attach inline IAM policies\n- create IAM access keys\n\nAWS IAM is the default target. When you need an IAM-compatible alternative, use\n`--iam-endpoint` or `iam_endpoint` in JSON config.\n\n---\n\n\u003ca id=\"deleting-buckets\"\u003e\u003c/a\u003e\n\n## 🧹 Deleting Buckets\n\nUse `--delete` with one or more `--bucket` values to remove buckets instead of\ncreating them. Empty buckets can be deleted without `--force`. Non-empty\nbuckets require `--force`; without it, `s3ctl` lists the bucket contents and\nrefuses to delete the bucket if objects, object versions, or delete markers are\npresent. Use `--dry-run` to preview the target.\n\n```bash\ns3ctl --bucket app-data --delete --dry-run\ns3ctl --bucket app-data --delete\ns3ctl --bucket app-data --delete --force --timeout 30m\n```\n\nWithout `--force`, the S3 provider only lists object versions, delete markers,\nand current objects to confirm the bucket is empty before deleting it. With\n`--force`, it deletes object versions and delete markers when the endpoint\nsupports version listing, deletes any remaining current objects, and finally\ndeletes the bucket.\nThe S3 principal running the delete needs the matching list, object delete,\nobject version delete, and bucket delete permissions.\n\nJSON config can also drive this mode:\n\n```json\n{\n  \"bucket\": \"app-data\",\n  \"delete_bucket\": true\n}\n```\n\nThe shorter `\"delete\": true` config key is accepted as an alias for\n`\"delete_bucket\": true`.\n\nKeep `\"force\": true` out of shared default configs unless every run using that\nconfig should be allowed to remove bucket contents before deleting buckets.\n\nUse `--timeout` or `\"timeout\": \"30m\"` for large buckets or slower\nobject-storage endpoints. The default timeout is `10m`.\n\n---\n\n\u003ca id=\"ovhcloud-notes\"\u003e\u003c/a\u003e\n\n## ☁️ OVHcloud Notes\n\nUse `--provider ovh` to create OVHcloud Object Storage through the Public Cloud\nAPI. OVHcloud calls buckets \"containers\"; `s3ctl` keeps the CLI wording as\nbucket because the resulting credentials are S3-compatible.\n\nThe OVHcloud provider creates one Public Cloud user and one S3 credential pair\nper bucket, creates the container in `--region`, attaches the user to that\ncontainer with the matching OVHcloud container profile (`readWrite` by default),\nand imports an OVHcloud S3 user policy scoped to that bucket. It does not apply\nS3 bucket policy documents; access is controlled through OVHcloud container\nprofiles and S3 user policies. The `replication` policy profile uses OVHcloud's\nnative `admin` container profile plus an imported S3 user policy that keeps\naccess scoped to the bucket and denies bucket-administration writes.\n\nThe generated OVHcloud user policy denies `s3:ListAllMyBuckets` so a bucket key\ncannot enumerate every bucket in the project. Use `mc ls alias/bucket-name` to\nlist objects in the bucket. Bare `mc ls alias` uses the S3 account-level bucket\nlisting API, which OVHcloud documents as all-buckets or denied rather than a\nreliable single-bucket filtered result.\n\nJSON output reports this OVHcloud container/S3 user policy as\n`scoped_access_policy_applied`. `bucket_policy_applied` is only emitted when an\nS3 bucket policy document was actually applied.\n\nFor `readOnly` and `readWrite`, `s3ctl` also adds explicit deny statements for\nunsupported operations on the owned bucket. OVHcloud currently falls back to the\nbucket owner's ACL when a user policy has no matching allow or deny, so explicit\ndenies are required for owner-scoped users.\n\nRequired OVHcloud settings:\n\n- `provider`: `ovh`\n- `ovh_service_name`: the Public Cloud project ID/service name\n- one OVHcloud auth mode: OAuth2 service account credentials, an access token,\n  classic OVH API application credentials, or standard go-ovh client discovery\n  such as `ovh.conf`\n- `region`: an OVHcloud Public Cloud/Object Storage region such as `UK`, `GRA`, `BHS`, `SBG`, or `EU-WEST-PAR`.\n  Use the uppercase region returned by OVHcloud's Public Cloud API. `s3ctl`\n  also accepts lowercase S3 endpoint regions such as `uk` and normalizes them\n  for OVHcloud API calls.\n\nOptional OVHcloud settings:\n\n- `ovh_api_endpoint`: endpoint name such as `ovh-eu`, `ovh-ca`, `ovh-us`, or a custom API URL\n- `ovh_client_id` and `ovh_client_secret`: OAuth2 service account credentials\n- `ovh_access_token`: short-lived OVHcloud access token\n- `ovh_application_key`, `ovh_application_secret`, and `ovh_consumer_key`: classic OVH API application credentials\n- `ovh_s3_endpoint`: override the returned S3 endpoint when the default\n  `https://s3.\u003cregion\u003e.io.cloud.ovh.net` form is not right for your project\n- `ovh_user_role`: defaults to `objectstore_operator`\n- `ovh_storage_policy_role`: one of `admin`, `deny`, `readOnly`, `readWrite`, or\n  `replication`. Use `replication` only for buckets that act as replication\n  targets; it allows bucket versioning/configuration reads and replication\n  target object actions supported by OVHcloud while remaining scoped to the\n  bucket.\n- `ovh_encrypt_data`: set to `true` to enable OVHcloud server-side encryption\n  with OVH-managed keys (`AES256` / SSE-OMK). When explicitly set to `false`,\n  `s3ctl` requests OVHcloud `plaintext` container storage.\n- `ovh_tags`: optional tags to apply to new OVHcloud containers. `s3ctl` does\n  not add tags by default. Use JSON config such as\n  `\"ovh_tags\": {\"environment\": \"prod\", \"owner\": \"platform\"}`, repeat\n  `--ovh-tag environment=prod --ovh-tag owner=platform`.\n- `ovh_rotate_credentials`: set to `true` to rotate S3 credentials for the\n  existing OVHcloud container owner instead of creating a new container. Keep it\n  out of the normal provisioning config unless every run should be a rotation.\n- `ovh_repair_policies`: set to `true` to reapply the OVHcloud container\n  profile and S3 user policy for existing bucket owners without issuing new\n  credentials.\n\n### 🔐 OVHcloud OAuth2 and IAM Setup\n\n```mermaid\nflowchart TD\n  admin[\"OVHcloud account or IAM admin\"] --\u003e oauth[\"Create OAuth2 service account\"]\n  admin --\u003e iam[\"Grant IAM policy on the Public Cloud project\"]\n  oauth --\u003e config[\"Add client ID and secret to s3ctl config\"]\n  project[\"Public Cloud project ID\"] --\u003e config\n  iam --\u003e access[\"Service account can manage Object Storage resources\"]\n  access --\u003e run[\"s3ctl --provider ovh --bucket app-data\"]\n  config --\u003e run\n  run --\u003e user[\"Create bucket-dedicated Public Cloud user\"]\n  run --\u003e bucket[\"Create Object Storage container in region\"]\n  run --\u003e userpolicy[\"Import bucket-scoped S3 user policy\"]\n  run --\u003e keys[\"Create S3 access key and secret\"]\n  user --\u003e policy[\"Attach container policy role\"]\n  bucket --\u003e policy\n  policy --\u003e userpolicy\n  userpolicy --\u003e keys\n  keys --\u003e result[\"Return endpoint, region, and credentials\"]\n```\n\nCreate the OAuth2 service account first. The official `ovhcloud` CLI is the\ncleanest route:\n\nInstall the CLI from OVHcloud's official guide:\n`https://help.ovhcloud.com/csm/en-cli-getting-started?id=kb_article_view\u0026sysparm_article=KB0072704`\n\n```bash\nbrew install --cask ovh/tap/ovhcloud-cli\n```\n\nWithout Homebrew:\n\n```bash\ncurl -fsSL https://raw.githubusercontent.com/ovh/ovhcloud-cli/main/install.sh | sh\n```\n\nAuthenticate it with your OVHcloud account:\n\n```bash\novhcloud login\n```\n\nThen create the service account credentials for `s3ctl`:\n\n```bash\novhcloud account api oauth2 client create \\\n  --name \"s3ctl\" \\\n  --description \"s3ctl bucket provisioning\" \\\n  --flow \"CLIENT_CREDENTIALS\"\n```\n\nOVHcloud returns a `clientId` and `clientSecret`; use those as `ovh_client_id`\nand `ovh_client_secret` in the `s3ctl` config.\n\nYou can also create the OAuth2 client from the OVHcloud API console. Open the\nconsole for your account region, go to `POST /me/api/oauth2/client`, and submit\nthis body:\n\n- EU: `https://eu.api.ovh.com/console/?branch=v1\u0026section=%2Fme`\n- CA: `https://ca.api.ovh.com/console/?branch=v1\u0026section=%2Fme`\n- US: `https://api.us.ovhcloud.com/console/?branch=v1\u0026section=%2Fme`\n\n```json\n{\n  \"callbackUrls\": [],\n  \"flow\": \"CLIENT_CREDENTIALS\",\n  \"name\": \"s3ctl\",\n  \"description\": \"s3ctl bucket provisioning\"\n}\n```\n\nNext, grant that service account access to the Public Cloud project. The service\naccount cannot grant access to itself; use the OVHcloud account/admin user or an\nexisting identity with IAM administration rights.\n\nIn OVHcloud Manager:\n\n1. Open **Identity, Security \u0026 Operations**.\n2. Open **Policies**.\n3. Create a policy named `s3ctl-object-storage`.\n4. Under **Identities**, select the `s3ctl` service account.\n5. Under **Product types**, select **Public Cloud Project**.\n6. Under **Resources**, select the project long ID shown under the project name,\n   for example `51ab2732562648349de40f72ac51c1c8`. Use this same value as\n   `ovh_service_name`; do not use the display name.\n7. For the first smoke test, authorise all actions on that selected project\n   resource. After confirming it works, tighten the policy to the actions below.\n\nLeast-privilege actions for `s3ctl`:\n\n- `publicCloudProject:apiovh:get`\n- `publicCloudProject:apiovh:user/create`\n- `publicCloudProject:apiovh:user/delete`\n- `publicCloudProject:apiovh:user/get`\n- `publicCloudProject:apiovh:user/policy/create`\n- `publicCloudProject:apiovh:user/s3Credentials/create`\n- `publicCloudProject:apiovh:user/s3Credentials/delete`\n- `publicCloudProject:apiovh:user/s3Credentials/get`\n- `publicCloudProject:apiovh:region/storage/create`\n- `publicCloudProject:apiovh:region/storage/delete`\n- `publicCloudProject:apiovh:region/storage/edit`\n- `publicCloudProject:apiovh:region/storage/get`\n- `publicCloudProject:apiovh:region/storage/policy/create`\n\nThe policy body in `examples/ovh-iam-policy.json` is a starting point for the\nAPI route, `POST /iam/policy`. Get the service account identity URN from\n`GET /me/api/oauth2/client/{clientId}`. OVHcloud documents the format as\n`urn:v1:\u003ceu|ca\u003e:identity:credential:\u003caccount-id\u003e/oauth2-\u003cclientId\u003e`. Get the\nproject resource URN from `GET /iam/resource` by selecting the\n`publicCloudProject` resource matching your Public Cloud project ID.\n\nVerify the policy before running `s3ctl`. With the same OAuth2 credentials,\n`GET /cloud/project` should list the project ID:\n\n```bash\ntoken=\"$(curl -fsS \\\n  -d grant_type=client_credentials \\\n  --data-urlencode \"client_id=$OVH_CLIENT_ID\" \\\n  --data-urlencode \"client_secret=$OVH_CLIENT_SECRET\" \\\n  -d scope=all \\\n  https://www.ovh.com/auth/oauth2/token | jq -r .access_token)\"\n\ncurl -fsS -H \"Authorization: Bearer $token\" \\\n  https://eu.api.ovh.com/1.0/cloud/project | jq .\n```\n\nExpected output should include the Public Cloud project ID:\n\n```json\n[\n  \"51ab2732562648349de40f72ac51c1c8\"\n]\n```\n\nIf OVHcloud returns `This service does not exist` while the project ID is\ncorrect, the service account usually cannot see the project yet. Recheck the IAM\npolicy identity, resource, and actions.\n\n### 🔄 OVHcloud Credential Rotation\n\nUse `--ovh-rotate-credentials` or `\"ovh_rotate_credentials\": true` when a bucket\nalready exists and you only want a fresh S3 access key and secret:\n\n```bash\ns3ctl --provider ovh --bucket app-data --ovh-rotate-credentials --output json\n```\n\nIf using JSON config for a rotation run:\n\n```json\n{\n  \"provider\": \"ovh\",\n  \"ovh_service_name\": \"PUBLIC_CLOUD_PROJECT_ID\",\n  \"ovh_client_id\": \"CLIENT_ID\",\n  \"ovh_client_secret\": \"CLIENT_SECRET\",\n  \"region\": \"UK\",\n  \"ovh_rotate_credentials\": true,\n  \"output\": \"json\"\n}\n```\n\nRotation looks up the existing container by bucket name, reads its `ownerId`,\nreapplies the container profile and scoped S3 user policy, creates a new S3\ncredential pair for that OVH Public Cloud user, then deletes the previous S3\ncredentials for that user. The new secret is only returned once, so store the\ncommand output securely. If an old key cannot be deleted after the new key is\ncreated, `s3ctl` still prints the new credentials and includes a warning so the\nstale key can be removed manually.\n\n### 🛠️ OVHcloud Policy Repair\n\nUse `--ovh-repair-policies` or `\"ovh_repair_policies\": true` when buckets and\nkeys already exist and you only need to reapply the scoped access policies:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket netspeedy-archives \\\n  --ovh-repair-policies \\\n  --output json\n```\n\nYou can pass multiple `--bucket` values or a batch file to repair several\nbucket users in one run. The command finds each bucket's `ownerId`, verifies the\nowner still looks bucket-dedicated, reapplies the OVHcloud container profile,\nand imports a generated S3 user policy for that bucket. It does not create,\ndelete, or rotate S3 access keys.\n\nTo widen a single bucket for replication target access without changing other\nbuckets, repair only that bucket with the `replication` profile:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket netspeedy-archives \\\n  --ovh-storage-policy-role replication \\\n  --ovh-repair-policies \\\n  --output json\n```\n\nFor already exposed credentials, prefer rotation after policy repair so old keys\nthat may have been copied elsewhere are removed:\n\n```bash\ns3ctl \\\n  --provider ovh \\\n  --bucket netspeedy-archives \\\n  --ovh-rotate-credentials \\\n  --output json\n```\n\n### 🗑️ OVHcloud Bucket Deletion\n\nOVHcloud buckets are containers, but the delete command still uses the bucket\nname:\n\n```bash\ns3ctl --provider ovh --bucket app-data --delete\n```\n\nFor OVHcloud deletes, `s3ctl` looks up the container owner, creates a temporary\nS3 credential for that OVH Public Cloud user, and checks whether the container\nis empty through the S3-compatible API. Empty containers are deleted without\n`--force`. Non-empty containers require `--force`, which allows `s3ctl` to empty\nthe container through the S3-compatible API before deleting it through the\nOVHcloud Public Cloud API. After the container is removed, `s3ctl` deletes the\nuser's S3 credentials and the OVH Public Cloud user. If the container is removed\nbut a final credential/user cleanup call fails, the command prints a warning so\nthe stale identity can be removed manually.\n\nFor safety, OVHcloud delete, credential rotation, and policy repair only\ncontinue when the container owner looks bucket-dedicated: the OVH Public Cloud\nuser description or username must match the bucket name, or the legacy\ndescription `s3ctl bucket \u003cbucket\u003e`. This prevents managing credentials or\npolicies on a shared manual OVH user.\n\nThe application key, application secret, and consumer key flow is still\nsupported as OVHcloud's classic API authentication path and can be used directly\nwith `s3ctl` as well.\n\nFor classic OVH API application credentials, use OVHcloud's token creation\npage. These links pre-fill the API rights `s3ctl` needs for Public Cloud bucket\nprovisioning, but they do not create OAuth2 service account credentials:\n\n- EU: `https://eu.api.ovh.com/createToken/?GET=%2Fcloud%2Fproject%2F%2A\u0026POST=%2Fcloud%2Fproject%2F%2A\u0026DELETE=%2Fcloud%2Fproject%2F%2A`\n- CA: `https://ca.api.ovh.com/createToken/?GET=%2Fcloud%2Fproject%2F%2A\u0026POST=%2Fcloud%2Fproject%2F%2A\u0026DELETE=%2Fcloud%2Fproject%2F%2A`\n- US: `https://api.us.ovhcloud.com/createToken/?GET=%2Fcloud%2Fproject%2F%2A\u0026POST=%2Fcloud%2Fproject%2F%2A\u0026DELETE=%2Fcloud%2Fproject%2F%2A`\n\nAfter creating the token, paste the returned application key, application\nsecret, and consumer key into `ovh_application_key`, `ovh_application_secret`,\nand `ovh_consumer_key`. To create `ovh_client_id` and `ovh_client_secret`,\nuse `POST /me/api/oauth2/client` instead.\n\n---\n\n\u003ca id=\"container\"\u003e\u003c/a\u003e\n\n## 🐳 Container\n\nBuild locally:\n\n```bash\nmake docker-build\ndocker run --rm s3ctl:dev --help\n```\n\nUse the published image:\n\n```bash\ndocker run --rm ghcr.io/soakes/s3ctl:latest --help\n```\n\nRun against the bundled examples from the host:\n\n```bash\ndocker run --rm \\\n  -v \"$PWD/examples:/examples:ro\" \\\n  ghcr.io/soakes/s3ctl:latest \\\n  --config /examples/s3ctl.json \\\n  --dry-run \\\n  --output json\n```\n\n---\n\n\u003ca id=\"development\"\u003e\u003c/a\u003e\n\n## 🛠️ Development\n\nCommon targets:\n\n```bash\nmake lint-install\nmake fmt\nmake lint\nmake vet\nmake test\nmake build\nmake refresh-go-toolchain\nmake build-release\nmake package-deb BINARY_PATH=dist/s3ctl-linux-amd64 DEB_ARCH=amd64\n```\n\nRecommended Go quality workflow:\n\n```bash\nmake lint-install\nmake fmt\nmake lint\nmake vet\nmake test\nmake build\n```\n\n`gofmt` is the baseline formatter. The pinned `golangci-lint` configuration adds `gofumpt`, `goimports`, `staticcheck`, `errcheck`, and `revive`.\n\nUse the website targets when changing the release hub so the local preview,\nmetadata fallback, and production build stay aligned.\n\nDependency updates are managed by Dependabot. Related AWS SDK for Go v2 module\nupdates are grouped into one pull request so shared `go.mod` and `go.sum`\nchanges do not create a queue of conflicting PRs. The Dependabot auto-merge\nworkflow runs after `Build and Validate` succeeds and on a daily maintenance\nschedule; when a Dependabot PR is conflicted or missing validation for its\ncurrent head revision, it comments `@dependabot rebase` once for that head and\nwaits for the refreshed branch to pass validation before merging.\n\n`make build-release` produces release archives in `dist/release/` for:\n\n- `linux/amd64`\n- `linux/arm64`\n- `linux/arm/v7`\n- `darwin/amd64`\n- `darwin/arm64`\n\nThe Linux binaries are built with `CGO_ENABLED=0`, so releases are architecture-specific rather than distro-specific and should run across most mainstream distributions for the same CPU family.\n\n---\n\n\u003ca id=\"release-process\"\u003e\u003c/a\u003e\n\n## 🚢 Release Process\n\nStable releases are published only after the project passes validation for\nformatting, linting, vetting, tests, build output, packaging, website assets,\nand CLI smoke checks.\n\nRelease candidates use tags such as `v1.2.3-rc.1` while a version is being\nvalidated. Production installs should use the latest stable release unless you\nare intentionally testing a candidate build.\n\nStable releases publish:\n\n- Linux and macOS archives for `amd64`, `arm64`, and Linux `arm/v7`\n- Debian packages for `amd64`, `arm64`, and `armhf`\n- a `SHA256SUMS` checksum file\n- GHCR images for the stable version, `latest`, and semver convenience tags\n- the GitHub Pages release hub with current installer and asset metadata\n- signed APT repository metadata for the stable channel\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoakes%2Fs3ctl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoakes%2Fs3ctl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoakes%2Fs3ctl/lists"}