Object Storage Security
Comprehensive security features to protect your data in DanubeData Object Storage.
Overview
DanubeData Object Storage provides multiple layers of security to ensure your data is protected at rest and in transit. This guide covers encryption, access control, and security best practices.
Encryption
Encryption at Rest
Object Storage can encrypt your objects server-side with AES-256. Encryption is a setting on each bucket:
- On by default for new buckets: A bucket created in the dashboard, API, CLI or Terraform uses SSE-S3 unless you turn it off. The bucket's Settings tab shows whether encryption is on.
- New objects only: Encryption applies to objects written while it is on. Objects stored before it was turned on stay unencrypted until they are uploaded again.
- Choice of keys: SSE-S3 uses keys the storage service manages. SSE-KMS uses a key you create under Security → KMS keys. SSE-C uses a key your client sends with each request. See S3 API Supported Actions.
- Client-side encryption: If the storage provider must never hold readable data, encrypt objects before upload (for example with rclone crypt, restic or Cryptomator).
Encryption in Transit
All connections use TLS 1.2 or 1.3:
- HTTPS only: Plain HTTP requests are redirected to HTTPS
- Strong cipher suites: Modern TLS configuration
- Certificate validation: Automatic certificate management
Access Control
Access Keys
Access keys are the primary method for authenticating to your buckets via the S3 API.
Creating Access Keys
Via Dashboard:
- Open Account → Security → S3 credentials and click Create access key. Or open your bucket, go to its Access tab and click Create a key for this bucket
- Enter a name and, under Access, choose All buckets (full access to every bucket in the team), Chosen buckets (the buckets you pick, each at a level you set) or By bucket policy (no access of its own: the key reaches only what a bucket's policy allows)
- Click Create access key, then confirm it's you when asked
- Copy the secret key immediately (shown only once)
Via API:
Access keys belong to your team. Without a scope, a key is team-wide and has full access to every bucket; to limit it, set scope to buckets and list the buckets it may use with a level for each, or set it to none for a key with no access of its own, which reaches only what a bucket policy allows.
curl -X POST https://danubedata.ro/api/v1/storage/access-keys \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-app-key",
"scope": "buckets",
"bucket_permissions": [
{ "bucket_id": "{bucket_id}", "level": "readwrite" }
]
}'
Permission Levels
| Level | Description | Operations |
|---|---|---|
| read | Read-only access | GetObject, ListBucket, HeadObject, and reads of the bucket's configuration |
| readwrite | Read and upload objects | Everything in read, plus PutObject and multipart uploads |
| full | Read, upload and delete | Everything in readwrite, plus DeleteObject and changes to the bucket's versioning, CORS and lifecycle rules |
Best Practices for Access Keys
- Use least privilege: Only grant permissions that are needed
- Rotate regularly: Create new keys and retire old ones periodically
- Set expiration dates: Use expiring keys for temporary access: a key is revoked automatically within five minutes of its expiry date
- Monitor usage: Check last-used timestamps to identify unused keys
- Never commit to code: Use environment variables or secrets management
Public Access Control
By default, all buckets are private. You can enable public read access for specific use cases.
Enabling Public Access
Via Dashboard:
- Navigate to your bucket
- Click Settings
- Toggle Public Access to enabled
- Confirm the security warning
Via API:
curl -X PUT https://danubedata.ro/api/v1/storage/buckets/{bucket_id} \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"public_access": true
}'
Public Access Warning
When public access is enabled:
- Anyone can read objects in your bucket
- Objects are accessible via direct URL
- Egress traffic will count against your quota
- Use for static websites, public assets only
Bucket Policies
For fine-grained access control, you can apply bucket policies from the bucket's Access tab. The most common use is an IP allow-list — restricting a bucket so its objects are only reachable from source IP ranges you trust (office, VPN, or application servers), expressed in CIDR notation. Another is giving one of your access keys access to a single folder of a bucket, so that several keys can share the bucket and each reach only its own part.
You can build a policy two ways:
- Visual editor — Choose an effect (Allow or Deny), actions, resources, and source-IP conditions without hand-writing JSON, or use the form Give a key access to a folder (see below).
- JSON editor — Paste or write policy statements directly, with live validation as you type.
Custom statements merge automatically with the bucket's public-access setting and any scoped access keys, so you never have to restate them. Built-in guardrails reject any policy that would stop you (or DanubeData) from managing the bucket's own policy, so you can always recover.
The policy editor is available on buckets hosted on the current high-durability storage endpoint.
A statement's principal can be * (everyone) or one of your team's own access keys, named by its ARN, like arn:aws:iam:::user/team-42-sk-ab12cd34 in the last example below. A statement that names a key of another team, an account id or a role is refused when you save it. A statement can also limit listing to a folder with a StringLike or StringEquals condition on s3:prefix. That condition is accepted only in an Allow statement whose actions are s3:ListBucket, s3:ListBucketVersions or both, and no other action: on any other request there is no prefix to compare, so the statement would grant nothing.
A policy can open a bucket to a key until it is changed again, so saving a bucket policy asks you to confirm it's you, and you get a security email when what you saved changed the policy.
You can read a bucket's custom statements, replace them, and give a key a folder from the CLI too, with danube storage policy get, set, and grant (CLI 1.7.0 or later). Over the CLI, a token cannot answer the question to confirm it's you: the owner of the token gets the security email instead. See Bucket policies from the CLI.
Give a Key Access to a Folder
The form Give a key access to a folder writes the policy statements for you. The key can be one created as By bucket policy, which has no access of its own, or one created for Chosen buckets.
- Open the bucket's Access tab and click Edit policy in the Bucket policy card.
- In the Visual editor, find the form Give a key access to a folder.
- Under Access key, choose the key. The list holds your team's active keys that have a storage user of their own, keys with no access included. Keys created for All buckets are not listed: they sign as the team and need no grant.
- Enter the Folder, for example
invoices/. To give access to the whole bucket instead, tick Whole bucket, which turns the folder field off. An empty folder is an error, never the whole bucket. - Choose the Access level: Read only, Read and write or Full access.
- Click Add to policy. The form adds two statements to Your statements. Nothing is saved yet.
- Click Save policy and confirm it's you when asked.
| Access level | What the key may do in the folder |
|---|---|
| Read only | s3:GetObject, s3:GetObjectVersion |
| Read and write | Everything in Read only, plus s3:PutObject, s3:AbortMultipartUpload, s3:ListMultipartUploadParts |
| Full access | Everything in Read and write, plus s3:DeleteObject, s3:DeleteObjectVersion |
Every level also lets the key list the folder and the folders below it. Whole bucket gives the level on every object in the bucket and lets the key list all of it.
The folder name is tidied before it goes into the policy: a leading slash is dropped and one trailing slash is added, so /invoices and invoices/ name the same folder. A name with * or ? is refused, because they are wildcards in a policy, and so is one with the two characters ${, which start a policy variable (a lone $ is fine), or with control characters or invisible formatting characters, such as a zero-width space. A name can be at most 1000 bytes long (an accented letter takes 2 bytes, an emoji 4).
If the key you choose already reaches the whole bucket through its bucket permissions, the form says so. A folder grant adds to a key's access, it does not limit it: to keep a key to a folder, create it as By bucket policy.
Your Statements
Below the form, Your statements lists every custom statement as one line: Allow or Deny, who, what and where. Every line has Remove. Statements that the visual editor can model, those for Everyone (*) on the bucket and its objects, with or without one source-IP condition, also have Edit; every other statement has Edit as JSON, a statement for Everyone on a folder or with another condition included. To change a grant, remove its statements and add it again with the form, or edit them as JSON.
A statement that names a key which was revoked, has expired or no longer exists says so: Revoked key, Expired key: being revoked or Unknown key. A key is revoked automatically within five minutes of its expiry date, so Expired key shows only until then. Once the key is revoked, the statements that name it grant nothing, and the editor marks them Revoked key.
Example: Allow Read from Specific IP
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::my-bucket/*"],
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}
Example: Deny Delete Operations
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:DeleteObject", "s3:DeleteBucket"],
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
}
]
}
Example: Give a Key Access to a Folder
This lets one of your team's access keys read and write the invoices/ folder of a bucket, and list that folder. It is exactly what the form above writes for that key and folder at the level Read and write. It takes two statements: the s3:prefix condition only exists on a listing request, so it cannot share a statement with the object actions. To write it yourself, paste it into the JSON editor with your own key's ARN and your bucket's name.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]},
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:PutObject",
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts"
],
"Resource": ["arn:aws:s3:::dd-42-projects/invoices/*"]
},
{
"Effect": "Allow",
"Principal": {"AWS": ["arn:aws:iam:::user/team-42-sk-ab12cd34"]},
"Action": ["s3:ListBucket", "s3:ListBucketVersions"],
"Resource": ["arn:aws:s3:::dd-42-projects"],
"Condition": {"StringLike": {"s3:prefix": ["invoices/*"]}}
}
]
}
As the principal, name a key created for Chosen buckets or By bucket policy. A key created for All buckets signs as the team, which already has full access to every bucket, so a grant for it would change nothing.
Folder grants have these limits:
- No listing of the bucket's root. The key can list
invoices/and the folders below it, but not the bucket's root, so point an S3 browser at the folder. - Only these actions. A folder grant does not include
s3:GetBucketLocation, the other bucket-configuration reads, ors3:ListBucketMultipartUploads(the storage gateway cannot limit that one to a prefix). A client that needs them takes extra statements, written in JSON. - Size. A bucket takes up to 100 custom statements within 20 KB. A grant is two statements, and a full-access grant takes about 540 to 650 bytes, so about 30 fit in the 20 KB. A read-only grant is shorter, so a few more fit. The 20 KB counts the policy as JSON text, where a slash takes 2 bytes and a character outside plain ASCII takes 6 bytes (12 for most emoji), so a folder name with accented letters or emoji uses more of it than its length suggests.
- Revoked keys. Revoking a key leaves its statements in the policy. They grant nothing, and the editor marks them.
- SFTP. A key that reaches a bucket only through its policy, with nothing chosen for that bucket, cannot back that bucket's SFTP user. SFTP uses a key that was given the bucket under Chosen buckets, or a key created for All buckets.
Object Lock (WORM)
Object Lock provides write-once-read-many (WORM) protection. When a bucket has Object Lock enabled, an object version can be locked so that it cannot be deleted or overwritten until its retention period expires — useful for regulatory compliance, ransomware protection, and tamper-proof audit trails.
Retention modes
| Mode | Who can remove or shorten the lock |
|---|---|
| Governance | Normal requests cannot delete or overwrite a locked version; a user with elevated permissions can shorten or remove the lock when genuinely needed. |
| Compliance | No one — including the bucket owner — can delete a locked version or shorten its retention until the retention period expires. |
Enabling Object Lock
- Enable at bucket creation. Object Lock can only be turned on when the bucket is created — it cannot be added to an existing bucket. Enabling it automatically enables versioning, which Object Lock requires.
- Default retention (optional). Set a default mode and a retention period (1–36,500 days) that applies automatically to every new object version.
Access keys and Object Lock
Object Lock works with bucket-scoped and team-wide access keys alike, but two operations are reserved for team-wide keys (which own the bucket):
| Operation | Bucket-scoped key | Team-wide key |
|---|---|---|
| Read the bucket's lock configuration, read and set per-object retention and legal holds | Yes (read-write and full) | Yes |
Change the bucket's default retention (PutObjectLockConfiguration) | No — use the dashboard | Yes |
| Bypass governance retention (delete a locked version early) | No | Yes |
A scoped key can therefore only ever make an object more protected, never less — which is what you want for a backup or archiving credential. Change the bucket's default retention from the bucket's Settings tab rather than over the API.
Using Object Lock with backup software
Backup products that offer immutable backups — Veeam Backup & Replication, Veeam Agent, and similar — check the bucket for Object Lock support before they will enable immutability. To set one up:
- Create the bucket with Object Lock enabled. It cannot be added later. Versioning is enabled automatically.
- Leave the default retention empty. Backup software sets its own retention on each object version; a bucket-wide default on top of that keeps data locked for longer than your backup policy expects and inflates storage cost.
- Create an access key with the
fulllevel — the software needs to delete expired restore points once their retention lapses. - In the backup software, add the bucket as an S3 Compatible object storage repository against
https://s3.danubedata.ro, then enable its immutability option.
Immutability means a restore point cannot be deleted before its retention expires — including by you. Size the bucket for the full retention window, and see the note below on storage consumed by locked versions.
Per-object controls
- Retention — Apply or extend a retention date on an individual object version with
PutObjectRetention. - Legal hold — Place an indefinite hold on a specific object version with
PutObjectLegalHold, independent of any retention period. The version cannot be deleted until you explicitly remove the hold.
# Apply Compliance-mode retention until a specific date
aws --endpoint-url https://s3.danubedata.ro s3api put-object-retention \
--bucket my-bucket \
--key important.log \
--retention '{"Mode":"COMPLIANCE","RetainUntilDate":"2027-01-01T00:00:00Z"}'
# Place a legal hold on an object
aws --endpoint-url https://s3.danubedata.ro s3api put-object-legal-hold \
--bucket my-bucket \
--key important.log \
--legal-hold '{"Status":"ON"}'
Because a locked object version consumes storage until its retention expires (and delete markers don't reclaim it), pair Object Lock with a realistic retention period to keep costs predictable.
SFTP Access
In addition to the S3 API, you can reach a bucket over SFTP — handy for legacy tools, batch jobs, or users that speak SFTP but not S3.
Host: sftp.new-s3.danubedata.ro
Port: 2222
- Create SFTP users from the bucket's SFTP section in the dashboard. Creating one asks you to confirm it's you, and we email you whenever SFTP access is enabled.
- Each SFTP user is bound to a single bucket and one of your S3 access keys, so its permissions follow that key (read-only, read-write, or full). This gives you per-key access control over SFTP, exactly as with the S3 API.
- The dashboard shows the exact username to use; the password is that access key's secret. Revoking or rotating the underlying access key immediately revokes the SFTP login too.
CORS Configuration
Configure Cross-Origin Resource Sharing (CORS) to allow web applications to access your bucket.
Why CORS?
Browsers block cross-origin requests by default. If your web application needs to upload or download files directly from Object Storage, you must configure CORS.
Configuring CORS
Via Dashboard:
- Navigate to your bucket
- Click Settings → CORS
- Add CORS rules
- Save changes
Via API:
curl -X PUT https://danubedata.ro/api/v1/storage/buckets/{bucket_id} \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cors_configuration": {
"CORSRules": [
{
"AllowedOrigins": ["https://myapp.com", "https://*.myapp.com"],
"AllowedMethods": ["GET", "PUT", "POST", "DELETE"],
"AllowedHeaders": ["*"],
"ExposeHeaders": ["ETag", "x-amz-meta-*"],
"MaxAgeSeconds": 3600
}
]
}
}'
CORS Rule Options
Each rule in CORSRules (up to 10) takes:
| Field | Description | Example |
|---|---|---|
AllowedOrigins | Domains allowed to make requests | ["https://myapp.com"] |
AllowedMethods | HTTP methods allowed: GET, PUT, POST, DELETE, HEAD | ["GET", "PUT"] |
AllowedHeaders | Request headers allowed | ["Content-Type", "Authorization"] |
ExposeHeaders | Response headers exposed to browser | ["ETag"] |
MaxAgeSeconds | How long browser caches preflight | 3600 |
CORS Best Practices
- Be specific with origins: Avoid using
*in production - Limit methods: Only allow methods your app needs
- Set appropriate max_age: Balance security and performance
- Test thoroughly: Use browser dev tools to verify CORS
Presigned URLs
Generate temporary, secure URLs for sharing objects without exposing credentials.
Download URL
import boto3
s3 = boto3.client(
's3',
endpoint_url='https://s3.danubedata.ro',
aws_access_key_id='YOUR_ACCESS_KEY',
aws_secret_access_key='YOUR_SECRET_KEY'
)
# Valid for 1 hour
url = s3.generate_presigned_url(
'get_object',
Params={'Bucket': 'my-bucket', 'Key': 'secret-file.pdf'},
ExpiresIn=3600
)
Upload URL
# Generate upload URL
upload_url = s3.generate_presigned_url(
'put_object',
Params={
'Bucket': 'my-bucket',
'Key': 'uploads/user-file.txt',
'ContentType': 'text/plain'
},
ExpiresIn=3600
)
# Client can upload using:
# curl -X PUT -H "Content-Type: text/plain" --data-binary @file.txt "$upload_url"
Presigned URL Security
- Time-limited: URLs expire after the specified duration
- Operation-specific: Each URL is valid for one operation
- Cannot be revoked: Once generated, valid until expiry
- Audit trail: Track which key generated the URL
Security Best Practices
1. Principle of Least Privilege
Create separate access keys for different applications with only the permissions they need:
API=https://danubedata.ro/api/v1
AUTH=(-H "Authorization: Bearer YOUR_API_TOKEN" -H "Content-Type: application/json")
# Read-only key for analytics
curl -X POST "$API/storage/access-keys" "${AUTH[@]}" \
-d '{"name": "analytics", "scope": "buckets", "bucket_permissions": [{"bucket_id": "{bucket_id}", "level": "read"}]}'
# Upload key: reads and writes, cannot delete
curl -X POST "$API/storage/access-keys" "${AUTH[@]}" \
-d '{"name": "uploader", "scope": "buckets", "bucket_permissions": [{"bucket_id": "{bucket_id}", "level": "readwrite"}]}'
# Full access for backups
curl -X POST "$API/storage/access-keys" "${AUTH[@]}" \
-d '{"name": "backup", "scope": "buckets", "bucket_permissions": [{"bucket_id": "{bucket_id}", "level": "full"}]}'
When several modules of one application share a bucket, give each module its own key, created as By bucket policy ("scope": "none" over the API), and give each key only its own folder with a folder grant in the bucket's policy. A leaked key then exposes one folder, and you do not need a bucket for every module.
2. Enable Versioning for Critical Data
Protect against accidental deletion or overwrites:
curl -X PUT https://danubedata.ro/api/v1/storage/buckets/{bucket_id} \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"versioning_enabled": true}'
3. Implement Lifecycle Rules
Automatically delete temporary files and old versions:
{
"rules": [
{
"id": "delete-temp-files",
"prefix": "temp/",
"expiration_days": 7
},
{
"id": "delete-old-versions",
"noncurrent_version_expiration_days": 30
}
]
}
4. Monitor Access Key Usage
Regularly review access keys:
- Check last-used timestamps
- Delete unused keys
- Rotate keys periodically
- Set expiration dates for temporary access: a key is revoked automatically within five minutes of its expiry date
5. Use Separate Buckets for Sensitivity
Organize data by sensitivity level:
production-public- Public assets, CDN contentproduction-private- Application data, user uploadsproduction-sensitive- PII, financial data (most restricted)
6. Audit and Logging
Enable access logging to track all bucket operations:
- Who accessed what objects
- When operations occurred
- Success/failure status
- Source IP addresses
Compliance
GDPR Compliance
DanubeData Object Storage is fully GDPR compliant:
- Data residency: All data stored in Germany (EU)
- Encryption: AES-256 server-side encryption (on by default for new buckets), TLS 1.2 or 1.3 in transit
- Access control: Fine-grained permission management
- Data deletion: Objects can be permanently deleted
- Audit trail: Complete access logging available
- Data Processing Agreement: Download a signed DPA (PDF) for your records from your account's legal settings
- Immutability: Use Object Lock in Compliance mode to enforce retention for records that must not be altered or deleted
Data Retention
Use lifecycle rules to implement data retention policies:
{
"rules": [
{
"id": "gdpr-retention",
"prefix": "user-data/",
"expiration_days": 365,
"noncurrent_version_expiration_days": 90
}
]
}
Troubleshooting
"Access Denied" Errors
- Verify credentials: Check access key and secret
- Check permissions: Ensure key has required permissions
- Bucket ownership: Verify key belongs to bucket's team
- Bucket policy: Check for deny rules blocking access
- Key with no access of its own: A key created as By bucket policy reaches only what the bucket's policy allows. Open the bucket's Access tab and click Edit policy, and check that a statement names the key, for the folder and level you expect; the key's drawer in Security → S3 credentials lists the buckets whose policy allows it. Also check that the key has not been revoked or reached its expiry date: a key is revoked automatically within five minutes of that date
CORS Errors in Browser
- Check origin: Ensure your domain is in allowed_origins
- Check method: Verify HTTP method is allowed
- Check headers: Ensure required headers are allowed
- Browser cache: Clear preflight cache and retry
Presigned URL Not Working
- Check expiry: URL may have expired
- Clock sync: Ensure server clock is accurate
- URL encoding: Don't manually modify the URL
- Key revocation: Original key may have been deleted
Next Steps
- Object Storage Product Overview - Full feature documentation
- Object Storage Quick Start - Get started guide
Questions? Contact support at support@danubedata.ro