Object Storage API
Manage S3-compatible object storage buckets and access keys programmatically through the DanubeData API.
Endpoints Overview
Storage Buckets
| Method | Endpoint | Description | Scope Required |
|---|---|---|---|
| GET | /api/v1/storage/buckets | List all buckets | storage:read |
| POST | /api/v1/storage/buckets | Create a new bucket | storage:write |
| GET | /api/v1/storage/buckets/{id} | Get bucket details | storage:read |
| PUT | /api/v1/storage/buckets/{id} | Update bucket settings | storage:write |
| DELETE | /api/v1/storage/buckets/{id} | Delete a bucket | storage:delete |
| GET | /api/v1/storage/buckets/{id}/metrics | Get bucket metrics | storage:read |
| GET | /api/v1/storage/buckets/{id}/metrics/trend | Historical time-series metrics | storage:read |
| GET | /api/v1/storage/buckets/{id}/metrics/top-objects | Top objects by size / egress / requests | storage:read |
| GET | /api/v1/storage/buckets/{id}/metrics/health | Bucket health (multipart, deleted versions) | storage:read |
Storage Access Keys
| Method | Endpoint | Description | Scope Required |
|---|---|---|---|
| GET | /api/v1/storage/access-keys | List all access keys | storage:read |
| POST | /api/v1/storage/access-keys | Create a new access key | storage:write |
| GET | /api/v1/storage/access-keys/{id} | Get access key details | storage:read |
| DELETE | /api/v1/storage/access-keys/{id} | Revoke an access key | storage:delete |
Bucket Policy
| Method | Endpoint | Description | Scope Required |
|---|---|---|---|
| GET | /api/v1/storage/buckets/{id}/policy | Get a bucket's custom policy statements and its effective policy | storage:read |
| PUT | /api/v1/storage/buckets/{id}/policy | Replace the custom policy statements | storage:read and storage:write |
| POST | /api/v1/storage/buckets/{id}/policy/folder-grants | Give an access key access to a folder | storage:read and storage:write |
Storage Buckets
List All Buckets
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"data": [
{
"id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
"name": "my-bucket",
"display_name": "My Application Bucket",
"status": "active",
"status_label": "Active",
"region": "fsn1",
"endpoint_url": "https://s3.danubedata.ro",
"public_access": false,
"versioning_enabled": false,
"encryption_enabled": true,
"size_bytes": 1073741824,
"size_human": "1.00 GB",
"object_count": 150,
"monthly_cost_cents": 500,
"monthly_cost_dollars": 5.00,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
}
],
"pagination": {
"current_page": 1,
"last_page": 1,
"per_page": 15,
"total": 1
}
}
Create a Bucket
curl -X POST 'https://danubedata.ro/api/v1/storage/buckets' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "my-new-bucket",
"display_name": "My New Bucket",
"region": "fsn1",
"versioning_enabled": false,
"public_access": false,
"encryption_enabled": true,
"tags": {
"environment": "production",
"project": "webapp"
}
}'
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Bucket name (3-63 chars, lowercase, alphanumeric and hyphens) |
display_name | string | No | Human-readable display name |
region | string | Yes | Region code (e.g., fsn1) |
versioning_enabled | boolean | No | Enable object versioning (default: false) |
public_access | boolean | No | Allow public read access (default: false) |
encryption_enabled | boolean | No | Enable server-side encryption (default: true) |
encryption_type | string | No | Encryption type: sse-s3 or sse-kms |
cors_configuration | array | No | CORS rules for the bucket |
lifecycle_rules | array | No | Object lifecycle rules |
tags | object | No | Key-value tags for the bucket |
Response (201 Created):
{
"message": "Storage bucket created successfully",
"bucket": {
"id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
"name": "my-new-bucket",
"status": "pending",
"status_label": "Pending"
}
}
Get Bucket Details
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"bucket": {
"id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
"name": "my-bucket",
"display_name": "My Application Bucket",
"status": "active",
"region": "fsn1",
"endpoint_url": "https://s3.danubedata.ro",
"public_url": null,
"public_access": false,
"versioning_enabled": false,
"encryption_enabled": true,
"encryption_type": "sse-s3",
"size_bytes": 1073741824,
"object_count": 150,
"custom_policy_statements": null,
"bucket_arn": "arn:aws:s3:::dd-42-my-bucket",
"tags": {
"environment": "production"
},
"monthly_cost_cents": 500,
"monthly_cost_dollars": 5.00,
"can_be_modified": true,
"can_be_destroyed": true,
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z"
},
"endpoint": "https://s3.danubedata.ro"
}
Update Bucket Settings
curl -X PUT 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"display_name": "Updated Bucket Name",
"versioning_enabled": true,
"public_access": false,
"tags": {
"environment": "staging"
}
}'
Response:
{
"message": "Bucket settings updated successfully",
"bucket": {
"id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
"name": "my-bucket",
"display_name": "Updated Bucket Name",
"versioning_enabled": true
}
}
Delete a Bucket
curl -X DELETE 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"message": "Bucket deletion initiated",
"status": "destroying"
}
Note: Bucket deletion is asynchronous. The bucket and all its objects will be permanently deleted.
Get Bucket Metrics
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"size_bytes": 1073741824,
"size_human": "1.00 GB",
"object_count": 150,
"requests_24h": 48213,
"requests_24h_by_method": { "GET": 41002, "PUT": 6100, "DELETE": 1111 },
"requests_24h_by_status": { "2xx": 47800, "4xx": 390, "5xx": 23 },
"error_rate_24h": 0.0086,
"latency_24h_ms": { "p50": 12.4, "p95": 58.1, "mean": 18.7 },
"egress_bytes_24h": 5368709120,
"egress_human_24h": "5.00 GB",
"ingress_bytes_24h": 1073741824,
"ingress_human_24h": "1.00 GB",
"monthly_cost_cents": 500,
"monthly_cost_dollars": 5.00,
"source": "deltas",
"freshness": "fresh",
"last_sync_at": "2024-01-15T12:00:00Z",
"metrics_precomputed_at": "2024-01-15T12:00:00Z"
}
Notes:
freshnessisfresh,lagging, orstale— how recent the metrics are.sourceisdeltaswhen the per-interval metrics pipeline has data for the bucket, orlegacyfor older buckets where the status/latency/ingress fields arenull. Skip absent dimensions whensourceislegacy.
Get Metrics Trend
Historical time-series for a bucket.
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/trend?window=24h' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Query parameters:
| Parameter | Values | Default | Description |
|---|---|---|---|
window | 1h, 6h, 12h, 24h, 3d, 7d, 14d, 30d | 24h | Time range |
resolution | 1m, 5m, 10m, 15m, 30m, 1h, 6h, 1d | auto (from window) | Bucket interval for each data point |
Response:
{
"bucket_id": "01HXYZ...",
"window": "24h",
"resolution": "5m",
"source": "deltas",
"freshness": "fresh",
"generated_at": "2024-01-15T12:00:00Z",
"data": [ ... ]
}
Get Top Objects
Top-N objects in a bucket by size, egress, or request count. Powered by an hourly probe — returns an empty items array until the first snapshot has run.
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/top-objects?dimension=size&limit=10' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Query parameters:
| Parameter | Values | Default | Description |
|---|---|---|---|
dimension | size, egress, requests | size | Ranking dimension |
limit | 1–50 | 10 | Number of objects to return |
Response:
{
"bucket_id": "01HXYZ...",
"dimension": "size",
"recorded_at": "2024-01-15T11:00:00Z",
"items": [
{ "rank": 1, "object_key": "backups/2024-01-15.tar.gz", "value": 2147483648 },
{ "rank": 2, "object_key": "media/video.mp4", "value": 734003200 }
]
}
Get Bucket Health
Reclaimable-space and freshness indicators. Values are null until the hourly health probe has visited the bucket.
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/metrics/health' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"bucket_id": "01HXYZ...",
"pending_multipart_count": 3,
"pending_multipart_bytes": 15728640,
"deleted_size_bytes": 524288000,
"freshness": "fresh",
"metrics_precomputed_at": "2024-01-15T12:00:00Z",
"last_health_check_at": "2024-01-15T11:00:00Z"
}
Storage Access Keys
Access keys provide S3-compatible credentials for accessing your storage buckets programmatically.
List All Access Keys
curl -X GET 'https://danubedata.ro/api/v1/storage/access-keys' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"data": [
{
"id": "abc12345-6789-0abc-def1-234567890abc",
"name": "Production API Key",
"access_key_id": "DDAK1234567890EXAMPLE",
"access_key_id_masked": "DDAK****XAMPLE",
"arn": null,
"status": "active",
"status_label": "Active",
"permissions": null,
"expires_at": null,
"last_used_at": "2024-01-15T11:30:00Z",
"is_expired": false,
"created_at": "2024-01-10T09:00:00Z",
"updated_at": "2024-01-15T11:30:00Z"
}
],
"pagination": {
"current_page": 1,
"last_page": 1,
"per_page": 15,
"total": 1
}
}
Create an Access Key
curl -X POST 'https://danubedata.ro/api/v1/storage/access-keys' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"name": "My Application Key",
"expires_at": "2025-01-15T00:00:00Z"
}'
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A descriptive name for the access key |
expires_at | datetime | No | Expiration date (ISO 8601 format) |
scope | string | No | team (the default) for a key that reaches every bucket, buckets for a key restricted to the buckets in bucket_permissions, or none for a key with no access of its own: it reaches only what a bucket policy allows it |
bucket_permissions | array | Conditional | Required if scope is buckets: 1 to 50 entries, each with a bucket_id (the id of one of your buckets, in the status active or updating; a bucket in any other status is refused with 422) and a level (read, readwrite or full). For team and none, leave it out or send an empty list. Send it only together with a scope: without one it is refused, not taken for a team-wide key |
Response (201 Created):
{
"id": "abc12345-6789-0abc-def1-234567890abc",
"name": "My Application Key",
"access_key_id": "DDAK1234567890EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"arn": null,
"expires_at": "2025-01-15T00:00:00Z",
"message": "Access key created. Make sure to save the secret key - it will not be shown again."
}
Important: The secret_access_key is only returned once during creation. Store it securely as it cannot be retrieved later.
arn: A key created with scope set to buckets or none gets a storage user of its own, and arn is that user's ARN, for example arn:aws:iam:::user/team-42-sk-k3j9x2mq. Name it as the principal of a statement in a bucket policy when the statement should apply to that key alone. arn is null for a key that signs as your team, an identity all such keys share. The list and detail responses carry the same field. See S3 Access Keys.
Get Access Key Details
curl -X GET 'https://danubedata.ro/api/v1/storage/access-keys/{access_key_id}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"access_key": {
"id": "abc12345-6789-0abc-def1-234567890abc",
"name": "My Application Key",
"access_key_id": "DDAK1234567890EXAMPLE",
"access_key_id_masked": "DDAK****XAMPLE",
"arn": null,
"status": "active",
"status_label": "Active",
"expires_at": "2025-01-15T00:00:00Z",
"last_used_at": "2024-01-15T11:30:00Z",
"is_expired": false,
"created_at": "2024-01-10T09:00:00Z"
}
}
Note: The secret_access_key is never returned after initial creation.
Revoke an Access Key
curl -X DELETE 'https://danubedata.ro/api/v1/storage/access-keys/{access_key_id}' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"message": "Access key has been revoked"
}
Warning: Revoking an access key immediately invalidates it. Any applications using this key will lose access.
Bucket Policy
A bucket's policy decides who can reach the bucket and what they may do there. It has two parts: the custom statements you write, which these endpoints read and replace, and the statements the platform manages for you (the public access setting, the buckets chosen for scoped access keys, and its own access), which you never restate. The effective policy is the whole document, both parts merged, that the storage gateway enforces. See Bucket Policies for what a statement can say.
The policy endpoints work on the buckets that have the policy editor in the console, which are the buckets on the current high-durability storage endpoint. For any other bucket they answer 404 Not Found, and so they do for a bucket that belongs to another project.
A change is saved at once and applied to the storage gateway in the background. PUT and POST answer 202 Accepted with status set to updating, which means the change is on its way. active means none is being applied any more, and a change the gateway refused ends there too, so active alone does not prove that the gateway holds the change. To be sure, read the bucket's policy with S3 GetBucketPolicy, or send the same request again, which sends the document to the gateway again.
A change that arrives while the bucket's policy is being applied waits for up to five seconds. If the bucket is still busy by then, nothing is saved and the answer is 409 Conflict with Retry-After: 5: send the request again.
{"error": "The bucket's policy is being applied. Try again in a moment."}
GET needs the storage:read ability. PUT and POST need both storage:read and storage:write, because their answer is the policy.
When a change alters the stored statements, the person whose API token made it gets a "bucket policy changed" security email that links to the bucket's Access tab. The console asks you to confirm it's you before it saves a policy. A token cannot answer that, so the email is the safeguard over the API, as it is when a token creates an access key. Repeating a PUT or a folder grant that changes nothing sends no email, but it still sets the bucket to updating and sends the document to the gateway again, which is also how you retry a change that did not reach it.
Get a Bucket's Policy
curl -X GET 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Accept: application/json'
Response:
{
"custom_policy_statements": [],
"effective_policy": {
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PlatformOwnerObjectTagging",
"Effect": "Allow",
"Principal": {"AWS": ["arn:aws:iam:::user/team-42"]},
"Action": [
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:PutObjectTagging",
"s3:DeleteObjectTagging"
],
"Resource": ["arn:aws:s3:::dd-42-projects/*"]
}
]
},
"status": "active"
}
| Field | Description |
|---|---|
custom_policy_statements | The custom statements stored for the bucket, as they were saved. An empty list when there are none |
effective_policy | The policy document the gateway enforces. Each custom statement carries a Sid of the form Custom1, Custom2 and so on, in the order stored. DanubeData adds its own management addresses to an IP allow-list (a Deny with NotIpAddress) at the gateway, and they are not listed here: the list in such a statement is the one you saved |
status | updating means a change is on its way to the gateway. active means none is being applied any more, and a change the gateway refused ends there too |
Get Bucket Details also returns the stored statements as custom_policy_statements, which is null when there are none, and the bucket's ARN as bucket_arn.
Replace the Custom Statements
curl -X PUT 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"custom_policy_statements": [
{
"Effect": "Deny",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::dd-42-projects/*"],
"Condition": {"NotIpAddress": {"aws:SourceIp": ["203.0.113.0/24"]}}
}
]
}'
Response (202 Accepted): the same document as Get a Bucket's Policy, with the new statements in custom_policy_statements and effective_policy and status set to updating.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
custom_policy_statements | array | Yes | The statements to store, as a list even when there is only one. They replace every custom statement the bucket has now, and an empty list removes them all |
A statement is checked exactly as the console checks it when you save a policy there:
EffectisAlloworDeny. ADenycannot use wildcards in its actions, and cannot name the actions that manage the policy itself, so a policy can never lock you or DanubeData out of the bucket.Principalis"*"or{"AWS": [...]}with the ARNs of your own project's access keys, which the access key endpoints return asarn. A key of another project, an account id or a role is refused.Resourceis optional and has to be the bucket (arn:aws:s3:::dd-42-projects) or objects in it. The bucket's ARN isbucket_arnin the bucket details.Conditionis optional:IpAddressorNotIpAddressonaws:SourceIp, orStringLikeorStringEqualsons3:prefix. Thes3:prefixconditions are accepted only in anAllowstatement whose actions ares3:ListBucket,s3:ListBucketVersionsor both.- A bucket takes at most 100 custom statements and 20 KB in all.
- A statement whose
Sidis one the platform sets itself, such asPublicReadGetObjectorScopedKeysFull, is dropped.
A statement that is refused comes back as a 422 that names it by its position in the list you sent, and nothing is saved:
{
"message": "Validation failed.",
"errors": {
"custom_policy_statements.0.Principal": [
"Principal \"arn:aws:iam:::user/team-7-sk-zz99zz99\" is not an access key of this team. Use \"*\" for everyone, or the ARN of one of this team's access keys (arn:aws:iam:::user/<access-key-uid>)."
]
}
}
Give a Key Access to a Folder
A folder grant is two statements: one that lets a key work on the objects under a folder, and one that lets it list that folder. This endpoint writes them for you, adds the ones that are not stored yet after the custom statements that are stored, and saves the result, so a script does not have to read the policy, edit it and send it back.
This example gives one key read and write access to the invoices/ folder of a bucket. Create the key with scope set to none first: it then has an identity of its own and no access until a policy grants some, so the folder is all it reaches.
curl -X POST 'https://danubedata.ro/api/v1/storage/access-keys' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{"name": "invoices-service", "scope": "none"}'
The response carries the key's id, which is what a grant names, and its arn. Then give it the folder:
curl -X POST 'https://danubedata.ro/api/v1/storage/buckets/{bucket_id}/policy/folder-grants' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-H 'Content-Type: application/json' \
-H 'Accept: application/json' \
-d '{
"key_id": "abc12345-6789-0abc-def1-234567890abc",
"folder": "invoices",
"level": "readwrite"
}'
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
key_id | string | Yes | The id of the access key, as the access key endpoints return it. It is not the key's S3 access_key_id |
folder | string | One of folder and whole_bucket | The folder to give access to, such as invoices or reports/2026. A leading or trailing slash makes no difference. It cannot contain * or ?, the two characters ${ (which start a policy variable; a lone $ is fine), control characters or invisible formatting characters, and may be at most 1000 bytes long |
whole_bucket | boolean | One of folder and whole_bucket | true, with no folder, to give access to the whole bucket. A missing or empty folder is never taken for the whole bucket |
level | string | Yes | read to get objects, readwrite to also add and overwrite them, or full to also delete them |
Response (202 Accepted), for a bucket that had no custom statements before:
{
"added_statements": [
{"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/*"]}}}
],
"warnings": [],
"custom_policy_statements": [
{"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/*"]}}}
],
"effective_policy": {
"Version": "2012-10-17",
"Statement": [
{"Sid": "PlatformOwnerObjectTagging", "Effect": "Allow", "Principal": {"AWS": ["arn:aws:iam:::user/team-42"]}, "Action": ["s3:GetObjectTagging", "s3:GetObjectVersionTagging", "s3:PutObjectTagging", "s3:DeleteObjectTagging"], "Resource": ["arn:aws:s3:::dd-42-projects/*"]},
{"Sid": "Custom1", "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/*"]},
{"Sid": "Custom2", "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/*"]}}}
]
},
"status": "updating"
}
added_statements are the statements this request added, and warnings says what the grant does not do (below). The other fields are the policy as it stands, as in Get a Bucket's Policy. When the change has reached the gateway, the key can read, write and list under invoices/, and aws s3 ls s3://dd-42-projects/invoices/ with its credentials works. A status of active does not prove that it has: see above for how to be sure.
How a grant behaves:
- It adds, it does not replace. The statements that are not stored yet go after the statements that are stored, and the whole list that results is checked as a replace is, including the limits of 100 statements and 20 KB. When a statement that is already stored no longer passes (the error names it by its position in
custom_policy_statements), fix it with a replace first. - Asking twice changes nothing. A statement that is stored already, with the same contents, is not added again, so repeating a request returns
added_statementsempty and sends no email. Another spelling of the same folder, such asinvoices/or/invoices, is the same grant. - Grants add up. Giving the same key and folder a higher
leveladds only the object statement for it, and the narrower one stays. To narrow access, replace the custom statements. - Which keys. The key has to belong to the project, be active and not expired, and have an identity of its own: its
arnis notnull. A key that signs as your team already has full access to every bucket, so a grant for it is refused. The answer is the same for a key that does not exist as for one that belongs to another project. - Keys that have access already. A key created for chosen buckets (
scopeset tobuckets) keeps its access to the whole of those buckets, and a folder grant adds to it: a grant never narrows a key. When the key you name reaches this bucket that way,warningssays so: "This key already reaches the whole bucket through its bucket permissions (read). The folder grant adds to that; it does not limit the key to the folder. To limit it to the folder, revoke this key and create a key with scope set to none." A key created withscopeset tononehas no access of its own, and reaches only what its grants give it.
Errors: a refused grant is a 422 that names the field, and nothing is saved:
{
"message": "Validation failed.",
"errors": {
"key_id": ["Choose an active access key of this team."]
}
}
Limits of Bucket Policies
- A bucket takes about 30 full-access folder grants. Custom statements are limited to 20 KB and a full-access grant takes roughly 500 to 650 bytes, so there are fewer with long bucket and folder names, and more with read-only grants.
- A key that is limited to a folder cannot list the root of the bucket. Point an S3 browser or client at the folder, as in
s3://dd-42-projects/invoices/. - A folder grant gives the object actions of its level and listing, and nothing else. It does not include
s3:GetBucketLocation, the other reads of the bucket's configuration, ors3:ListBucketMultipartUploads. A client that needs them takes statements of its own, which you can add by replacing the custom statements. - Revoking an access key leaves its statements in the policy. They grant nothing, and you can remove them by replacing the custom statements.
Using Access Keys with S3 Clients
Once you have created an access key, you can use it with any S3-compatible client.
AWS CLI
aws configure --profile danubedata
# Enter your credentials when prompted:
# AWS Access Key ID: DDAK1234567890EXAMPLE
# AWS Secret Access Key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
# Default region name: fsn1
# Use with custom endpoint
aws s3 ls s3://my-bucket \
--endpoint-url https://s3.danubedata.ro \
--profile danubedata
Python (boto3)
import boto3
s3 = boto3.client(
's3',
endpoint_url='https://s3.danubedata.ro',
aws_access_key_id='DDAK1234567890EXAMPLE',
aws_secret_access_key='wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
region_name='fsn1'
)
# List objects in a bucket
response = s3.list_objects_v2(Bucket='my-bucket')
for obj in response.get('Contents', []):
print(obj['Key'])
JavaScript (AWS SDK v3)
import { S3Client, ListObjectsV2Command } from '@aws-sdk/client-s3';
const s3 = new S3Client({
endpoint: 'https://s3.danubedata.ro',
region: 'fsn1',
credentials: {
accessKeyId: 'DDAK1234567890EXAMPLE',
secretAccessKey: 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
},
forcePathStyle: true
});
const command = new ListObjectsV2Command({ Bucket: 'my-bucket' });
const response = await s3.send(command);
console.log(response.Contents);
Error Responses
422 Unprocessable Entity
{
"error": "You have reached the maximum number of buckets (10). Please delete a bucket to create a new one."
}
{
"error": "Bucket cannot be modified in its current state",
"status": "creating"
}
404 Not Found
{
"error": "Bucket not found"
}
403 Forbidden
Returned when your API token lacks the required storage:* scope.
{
"message": "Insufficient permissions"
}
Rate Limits
Storage API endpoints have specific rate limits:
| Endpoint | Limit |
|---|---|
| Create bucket | 40/min, 200/hour, 2000/day |
| Create access key | 20/min, 100/hour, 1000/day |
| Get metrics | 2x standard limits |
| Other endpoints | Standard tier limits |
See Rate Limits for more details.