Amazon S3 is the most common destination for asset migrations — and the easiest one to get wrong from a security perspective. This guide walks through creating a least-privilege IAM user scoped to just the buckets you care about. Do not use your AWS account root keys for this. Ever.
Step 1 — Sign in as an admin user (not root)
Open the IAM console as an admin user. If you're still using root, this is your sign to create an admin user first.
Step 2 — Create the IAM user
Users → Create user
Name it something specific likeasset-migrator-prod-s3. Do not check "Provide user access to the AWS Management Console" — this user only needs programmatic API access.Permissions: Attach policies directly
We'll attach a bucket-scoped inline policy in step 4. For now, click Next without picking any managed policy.Tags (optional)
Tag with something likepurpose=migrationfor cost-allocation reports.Create user
Finish the wizard.
Step 3 — Generate access keys
Click into the new user → Security credentials → Create access key.
- Use case: Application running outside AWS
- Acknowledge the recommendation
- Optionally label the key (helps later when rotating)
AWS shows the Access key ID and Secret access key once and only once. Copy both immediately — there's no way to view the secret again after you close this screen. If you lose it, you'll have to delete the key and create a new one.
Step 4 — Attach a least-privilege bucket policy
This is the step that distinguishes a secure migration from a security audit finding.
Back at the user, click Add permissions → Create inline policy → JSON, and paste a policy scoped to just the bucket(s) you'll migrate:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListBucket",
"Effect": "Allow",
"Action": ["s3:ListBucket", "s3:GetBucketLocation"],
"Resource": "arn:aws:s3:::my-bucket"
},
{
"Sid": "ObjectAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}Replace my-bucket with your bucket name (twice — note the /* on the second one).
Save the policy with a name like AssetMigrator-MyBucket-Access.
Step 5 — Verify with the AWS CLI (optional)
If you want to confirm the keys work before pasting them into Univaultport, install the AWS CLI and run:
aws s3 ls s3://my-bucket \
--profile asset-migrator-testAfter configuring ~/.aws/credentials with the new keys under that profile. A directory listing confirms s3:ListBucket works.
Step 6 — Paste credentials into Univaultport
Access Key ID
TheAKIA...string.Secret Access Key
The long secret value you copied earlier.Region
The bucket's region, e.g.us-east-1oreu-west-2. Find it in the S3 console under bucket properties.Bucket
The bucket name only — nos3://prefix.Key Prefix (optional)
Scopes this connector to a sub-path. Example:migrations/2026/. Listings and writes happen under this prefix.
Click Save & test. Univaultport runs a HeadBucket call to verify region and permissions in one round-trip.
Buckets vs. regions
S3 keys are global to your AWS account, but every API call is region-specific. If your bucket is in eu-central-1 but you put us-east-1 in the connector, you'll get redirect errors. Always check the bucket's actual region in the AWS console.
For migrations into a new destination bucket, AWS recommends creating it in the same region as the source data to minimize egress costs and latency.
Cost considerations
Pricing for typical migrations (as of mid-2026):
| Operation | Cost |
|---|---|
| Storage (Standard) | $0.023/GB/month |
| PUT requests | $0.005 per 1,000 |
| GET requests | $0.0004 per 1,000 |
| Egress to other AWS regions | $0.02/GB |
| Egress to internet | $0.09/GB (first 10 TB) |
For a one-time 100 GB migration:
- Storage cost while resident: ~$2.30/mo
- Request cost: < $1
- Egress (if leaving AWS): ~$9
If you're moving data into S3 from outside AWS, ingress is free.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
403 InvalidAccessKeyId | Key was deleted or never existed | Generate new keys |
403 SignatureDoesNotMatch | Wrong secret, or clock skew on the worker | Re-copy secret; sync NTP |
301 PermanentRedirect | Region mismatch | Set the correct region in the connector |
404 NoSuchBucket | Typo or wrong account | Check bucket name and that it's in the same AWS account as the IAM user |
403 AccessDenied on list | Bucket policy missing s3:ListBucket | Add it to the inline policy |
Rotating keys
AWS recommends rotating access keys every 90 days. The clean rotation flow:
- Create a second access key for the same IAM user (AWS allows two simultaneously).
- Update the Univaultport connector with the new key.
- Run a test migration to confirm.
- Delete the old key.
Never deactivate or delete the old key until you've verified the new one works — there's no rollback if you lock yourself out.
Next steps
With S3 connected, you can migrate to or from any other Univaultport connector. Common patterns:
- Cloudinary → S3 for cheaper archive storage of legacy DAM libraries
- S3 → S3 for cross-region or cross-account replication
- Local → S3 for cloud onboarding of on-prem assets
If your team uses AWS Organizations with cross-account access, you can also use a single IAM user with AssumeRole policies, though that flow currently requires using a base set of keys plus role ARN — DM us if you need that and we'll prioritize the feature.



