Skip to content

Bucket on AWS S3 ​

End-to-end: create an S3 bucket and a scoped IAM key, save it as a vsync profile, then run the daily loop. Everything here is copy-paste; replace acme-vault, the region, and the account ID with yours.

Prereqs: the aws CLI authenticated as an admin for the bucket-creation steps only. vsync itself never calls the aws CLI — it talks S3 directly with the access key you generate below.

1. Create the bucket ​

bash
# us-east-1 is special — it rejects a LocationConstraint:
aws s3api create-bucket --bucket acme-vault --region us-east-1

# Any other region needs the constraint:
aws s3api create-bucket --bucket acme-vault --region eu-central-1 \
  --create-bucket-configuration LocationConstraint=eu-central-1

Lock it down — this bucket holds (encrypted) secrets:

bash
aws s3api put-public-access-block --bucket acme-vault \
  --public-access-block-configuration \
  BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Optional belt-and-suspenders — bucket-level versioning on top of vsync's own:
aws s3api put-bucket-versioning --bucket acme-vault \
  --versioning-configuration Status=Enabled

2. Create a scoped IAM user + access key ​

Don't reuse your personal keys. Make a dedicated machine user whose only power is this one bucket.

bash
aws iam create-user --user-name acme-vault-bot

Save this policy as acme-vault-policy.json (the ListBucket statement powers vsync versions; Get/Put power pull/push/audit):

json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListTheBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::acme-vault"
    },
    {
      "Sid": "ReadWriteObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": "arn:aws:s3:::acme-vault/*"
    }
  ]
}

Attach it inline and mint an access key:

bash
aws iam put-user-policy --user-name acme-vault-bot \
  --policy-name acme-vault-rw \
  --policy-document file://acme-vault-policy.json

aws iam create-access-key --user-name acme-vault-bot

The output's AccessKeyId and SecretAccessKey are what you'll paste into the profile. The secret is shown once — copy it now.

3. Save it as a vsync profile ​

bash
vsync profile add acme-aws

Answer the prompts:

PromptValue for AWS S3
S3 endpoint URLhttps://s3.eu-central-1.amazonaws.com (your region)
S3 regioneu-central-1 (the real region — not auto)
S3 bucket nameacme-vault
S3 access key IDthe AccessKeyId from step 2
S3 secret access keythe SecretAccessKey from step 2
Optional prefixleave empty, or acme/ to share the bucket across repos

Verify:

bash
vsync profile list
vsync profile show acme-aws        # secret stays masked

Reuse the profile. You create it once per machine and bind as many repos/envs to it as you like — the creds get copied into each env's config at init time:

bash
# different envs of the same repo:
vsync init dev  --profile=acme-aws
vsync init prod --profile=acme-aws

# a totally different repo, same bucket (a prefix keeps them apart):
cd ../other-project
vsync init prod --profile=acme-aws

4. Bind an env and push ​

bash
cd my-project
vsync init prod --profile=acme-aws    # generates the AES key, writes config + .vsync pin

Put secrets in the vault folder, then push:

bash
cat > infra/vault/prod/.env.prod <<'EOF'
DATABASE_URL=postgres://user:pass@host/db
API_KEY=sk-...
EOF

vsync push prod

That uploads s3://acme-vault/<repo>/prod/versions/<ts>.enc and flips …/prod/latest to it. Confirm:

bash
vsync versions prod
vsync audit prod

5. Daily loop ​

Identical to every other provider — the bucket is never mentioned again:

bash
vsync pull prod                 # get the latest vault before you start
# … edit infra/vault/prod/ …
vsync push prod                 # ship your changes (encrypted, versioned, audited)
vsync use prod                  # ./.env → infra/vault/prod/.env.prod  (dotenv.config() just works)

vsync versions prod             # list every encrypted version on the bucket
vsync audit prod                # who pushed/pulled/exported, when

6. Fan out to GitHub Actions — sync gh ​

Push the vault's key/values into your repo's GitHub Actions secrets so CI has them. Routing (--gh-repo) is saved to the env config on first run, so later runs are just vsync sync prod gh:

bash
vsync sync prod gh --gh-repo=acme/web

# refine which keys go up (both flags repeatable; omit for "send everything as-is"):
vsync sync prod gh --gh-repo=acme/web \
  --exclude-property=LOCAL_ONLY \
  --inline-file-suffix=_FILE        # FOO_FILE=path  →  uploads the file's contents as secret FOO

Requires the gh CLI authenticated (gh auth login). Other targets — gcp, aws, azure, vault — work the same way; see Fanout to where prod runs.

7. Onboard a new dev ​

The new dev never gets the bucket creds — you hand them one encrypted .share file:

bash
# You (already set up):
vsync export prod                          # → ./<repo>-prod.share  + a one-time passphrase (printed once)

Send the .share file and the passphrase on two different channels (file via Slack, passphrase via SMS/password manager).

bash
# New dev, in the cloned repo:
vsync import prod ./<repo>-prod.share      # paste the passphrase when prompted
vsync pull prod                            # decrypt + unpack into infra/vault/prod/
vsync use prod                             # ./.env → infra/vault/prod/.env.prod

Done — their dotenv.config() reads ./.env. Full teammate flow: Onboarding teammates.

Rotating the key ​

When you rotate the IAM access key, update the profile and every env keeps working. See the IAM rotation runbook.

Troubleshooting ​

SymptomLikely cause
AccessDenied on pushPolicy missing s3:PutObject on arn:…/*, or wrong bucket ARN.
AccessDenied on vsync versionsMissing the s3:ListBucket statement (it's on the bucket ARN, no /*).
PermanentRedirect / region errorsregion in the profile doesn't match the bucket's actual region.
Works for you, fails for a teammateThey imported the .share (config + key) but their profile/creds are irrelevant — the config carries its own creds. Re-export/import if the bucket moved.

Released under the MIT License.