Skip to content

Object Storage (S3)

Follow this guide top to bottom on an Ubuntu workstation and you will go from nothing to a verified object-storage round trip in a single production session. You install the two CLI tools, load your Voyager application credentials, create one temporary S3 key pair, run a small bucket and object smoke test, and revoke the key when you finish.

Prerequisites

Before you begin, make sure you have completed the onboarding flow and your account is fully activated in Voyager. You also need an Ubuntu-based workstation where you can install the tools used in this guide and where you can download the Voyager OpenRC credentials file.

About Object Storage and the Production Endpoint

DevOpsCentral Object Storage is backed by Ceph and exposed through its S3-compatible gateway (Ceph RGW). Because the service speaks the S3 API, you can use standard S3 clients such as the AWS CLI while your data remains on the same locally owned infrastructure in Romania as your other DevOpsCentral resources.

Start by exporting the production S3 endpoint once for your shell session. Every aws command in this guide reads it from the S3_ENDPOINT environment variable:

export S3_ENDPOINT="https://devopscentral.cloud:6780"
Expected result
No output.

Example outputs are sanitized

The endpoint above is the real production endpoint; the Expected result blocks that follow show realistic but fictional examples. Your token expiry, IDs, timestamps, bucket name, package versions, and any filesystem-specific values will differ.

Development sandbox is a separate environment

DevOpsCentral also runs a development sandbox whose S3 endpoint is https://radosgw.virtomat.dev. It is a separate environment with its own projects and credentials. Do not mix sandbox and production commands; create the EC2 credentials from the OpenRC file of the environment you actually intend to use. This guide is the production path.

The service signs every S3 request in the us-east-1 region. That region is not the RegionOne value found in your OpenRC file, which belongs to the OpenStack API rather than to S3. Two rules follow from this:

  • Configure the AWS region as us-east-1 (the guide does this below).
  • When you create a bucket, omit LocationConstraint entirely.

Install the OpenStack CLI

Install the OpenStack client with apt:

sudo apt update
sudo apt install -y python3-openstackclient
Expected result
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://security.ubuntu.com/ubuntu noble-security InRelease
Hit:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  python3-openstackclient
0 upgraded, 1 newly installed, 0 to remove and 5 not upgraded.
Need to get 430 kB of archives.
After this operation, 5,959 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu noble/universe amd64 python3-openstackclient all 6.6.0-0ubuntu2 [430 kB]
Fetched 430 kB in 1s (215 kB/s)
Selecting previously unselected package python3-openstackclient.
(Reading database ... 187662 files and directories currently installed.)
Preparing to unpack .../python3-openstackclient_6.6.0-0ubuntu2_all.deb ...
Unpacking python3-openstackclient (6.6.0-0ubuntu2) ...
Setting up python3-openstackclient (6.6.0-0ubuntu2) ...

Install the AWS CLI

Install the AWS CLI with apt:

sudo apt update
sudo apt install -y awscli
Expected result
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://security.ubuntu.com/ubuntu noble-security InRelease
Hit:3 http://archive.ubuntu.com/ubuntu noble-updates InRelease
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
The following NEW packages will be installed:
  awscli
0 upgraded, 1 newly installed, 0 to remove and 5 not upgraded.
Need to get 10,255 kB of archives.
After this operation, 120,892 kB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu noble/universe amd64 awscli all 2.23.6-1 [10,255 kB]
Fetched 10,255 kB in 4s (2,564 kB/s)
Selecting previously unselected package awscli.
(Reading database ... 187718 files and directories currently installed.)
Preparing to unpack .../awscli_2.23.6-1_all.deb ...
Unpacking awscli (2.23.6-1) ...
Setting up awscli (2.23.6-1) ...

Verify the installation:

aws --version
Expected result
aws-cli/2.23.6 Python/3.12.3 Linux/6.8.0-57-generic botocore/2.23.6

Load Your Application Credentials

In Voyager, open the Quick Start section and download your OpenRC credentials file. Save it as open.rc under your home Downloads directory, then load it into your shell exactly as shown:

source ~/Downloads/open.rc
Expected result
No output.

Verify the OpenStack client can authenticate. This confirms the credentials are loaded before you create the S3 key pair:

openstack token issue
Expected result
+------------+------------------------------------------------------------------+
| Field      | Value                                                            |
+------------+------------------------------------------------------------------+
| expires    | 2026-09-08T14:22:41+0000                                         |
| project_id | 3f4a1c8e2b6d4a5f9e7c8b1d2a3e4f5a                                 |
| user_id    | c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4 |
+------------+------------------------------------------------------------------+

Your expires timestamp and the project_id and user_id values will be different; the table above is a formatted example.

Create EC2 Credentials (the S3 Key Pair)

DevOpsCentral Object Storage authenticates S3 clients with a key pair that OpenStack calls EC2 credentials: an access key ID and a secret key. The access key identifies you; the secret key signs your requests. Treat both as a password: never store them in shell history, never commit them to Git, and never share them in chat or email. The secret key is shown only once and cannot be retrieved again, so if it leaks or is lost you delete the credential and create a fresh one.

Create one temporary credential:

openstack ec2 credentials create -f json
Expected result
{
    "access": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
    "project_id": "3f4a1c8e2b6d4a5f9e7c8b1d2a3e4f5a",
    "secret": "f0e1d2c3b4a5968778695a4b3c2d1e0f",
    "trust_id": null,
    "user_id": "c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4"
}

Paste the access key ID and secret key from that output into the two prompts below. The secret prompt hides what you type:

read -rp "Access key ID: " AWS_ACCESS_KEY_ID
read -rsp "Secret access key: " AWS_SECRET_ACCESS_KEY
printf '\n'
export AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY
Expected result
Access key ID:
Secret access key:

The key pair now lives only in environment variables in this shell. Because you typed the values into prompts rather than onto the command line, they were not recorded in shell history.

Use Temporary AWS Configuration (No Profile)

This smoke test uses ephemeral settings only, so nothing is written to ~/.aws and no named profile is created. Create a private working directory that holds both the temporary AWS config and the test object, then tell the CLI where the temporary config lives and which region to sign with:

WORKDIR="$(mktemp -d)"
export AWS_CONFIG_FILE="$WORKDIR/config"
export AWS_DEFAULT_REGION="us-east-1"
aws configure set s3.addressing_style path
Expected result
No output.

AWS_DEFAULT_REGION is us-east-1 because that is the S3 signing region. Confirm the path-style setting, which DevOpsCentral requires:

aws configure get s3.addressing_style
Expected result
path

The credentials you typed remain only in the current shell, and the path-style setting is written to $WORKDIR/config. Both disappear during cleanup, when the working directory is removed and the variables are unset.

Verify S3 Access

Run a first authenticated request to list the buckets in your project. This request lists whatever buckets already exist in your project; the example below shows a project that has none yet:

aws --endpoint-url "$S3_ENDPOINT" s3api list-buckets
Expected result
{
    "Buckets": []
}

Create a Scratch Bucket

Bucket names are globally unique across the service and may contain only lowercase letters, numbers, and hyphens. Generate a collision-resistant scratch name from a prefix, a UTC timestamp, and a random suffix:

BUCKET="devopscentral-s3-smoke-$(date -u +%Y%m%d%H%M%S)-$RANDOM"
printf '%s\n' "$BUCKET"
Expected result
devopscentral-s3-smoke-20260908142305-18453

Create the bucket. Do not send a LocationConstraint, because the service reports the us-east-1 region:

aws --endpoint-url "$S3_ENDPOINT" s3api create-bucket --bucket "$BUCKET"
Expected result
{
    "Location": "/devopscentral-s3-smoke-20260908142305-18453"
}

Verify that exactly this bucket is reachable, without listing other buckets:

aws --endpoint-url "$S3_ENDPOINT" s3api head-bucket --bucket "$BUCKET"
Expected result
No output.

Upload and Verify an Object

Create a small local text payload inside the private working directory:

printf 'hello object storage\n' > "$WORKDIR/hello.txt"
Expected result
No output.

Upload it as hello.txt:

aws --endpoint-url "$S3_ENDPOINT" s3api put-object --bucket "$BUCKET" --key hello.txt --body "$WORKDIR/hello.txt"
Expected result
{
    "ETag": "\"6950c8884b3336d1f7dae1fe475b857e\""
}

List only the object you uploaded:

aws --endpoint-url "$S3_ENDPOINT" s3api list-objects --bucket "$BUCKET" --prefix hello.txt
Expected result
{
    "Contents": [
        {
            "ETag": "\"6950c8884b3336d1f7dae1fe475b857e\"",
            "Key": "hello.txt",
            "LastModified": "2026-09-08T14:23:17.000Z",
            "Size": 21,
            "StorageClass": "STANDARD"
        }
    ]
}

Read back the stored metadata for the exact object:

aws --endpoint-url "$S3_ENDPOINT" s3api head-object --bucket "$BUCKET" --key hello.txt
Expected result
{
    "AcceptRanges": "bytes",
    "ContentLength": 21,
    "ContentType": "binary/octet-stream",
    "ETag": "\"6950c8884b3336d1f7dae1fe475b857e\"",
    "LastModified": "2026-09-08T14:23:17.000Z",
    "Metadata": {}
}

Download the object to a distinct path in the same private directory and confirm the bytes match. The download command sends get-object's metadata summary to /dev/null; the byte comparison is the real check:

aws --endpoint-url "$S3_ENDPOINT" s3api get-object --bucket "$BUCKET" --key hello.txt "$WORKDIR/hello-download.txt" >/dev/null
cmp "$WORKDIR/hello.txt" "$WORKDIR/hello-download.txt" && printf 'Objects match\n'
Expected result
Objects match

Clean Up

Delete exactly the object you uploaded and delete exactly the bucket you created: never run broad or scripted deletion over buckets or credentials you do not own. Keep the EC2 credential for the next step, which still needs it to confirm the bucket is gone:

aws --endpoint-url "$S3_ENDPOINT" s3api delete-object --bucket "$BUCKET" --key hello.txt
aws --endpoint-url "$S3_ENDPOINT" s3api delete-bucket --bucket "$BUCKET"
Expected result
No output.

Confirm that the bucket is really gone while the EC2 credential is still valid, so a failed head-bucket proves the bucket is absent rather than that the credential is invalid. Only then revoke exactly the EC2 credential you created and confirm it is gone too, before removing the temporary working directory (which also removes the temporary AWS config) and clearing every variable the smoke test created. The checks exit nonzero if anything still exists, so a passing run ends with exactly this message:

if aws --endpoint-url "$S3_ENDPOINT" s3api head-bucket --bucket "$BUCKET" >/dev/null 2>&1; then
    echo "Bucket still exists" >&2
    exit 1
fi
openstack ec2 credentials delete "$AWS_ACCESS_KEY_ID"
if openstack ec2 credentials show "$AWS_ACCESS_KEY_ID" >/dev/null 2>&1; then
    echo "EC2 credential still exists" >&2
    exit 1
fi
rm -rf "$WORKDIR"
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_DEFAULT_REGION AWS_CONFIG_FILE S3_ENDPOINT BUCKET WORKDIR
printf 'Smoke test cleanup complete\n'
Expected result
Smoke test cleanup complete

Because this smoke test revokes the EC2 credential when it finishes, a later S3 session starts from the beginning again: create a fresh EC2 credential with openstack ec2 credentials create, type it into the prompts, and clean it up the same way when you are done.