In this article we show you how to create an empty bucket (container) in an S3-compatible Object Store on Linux using curl and openssl (and xxd), without additional tools such as AWS CLI or s3cmd. We use AWS Signature v4 for authentication; the required signature is generated for you by a shell script.
For the steps in this guide you need:
- An Object Store project with an S3 token. From the S3 token you need the ‘Access Key’, ‘Secret Key’ and ‘Project ID’. If you have not yet created an S3 token (default option during the ordering process) or want to retrieve your S3 token details, take a look at our article Managing S3 tokens.
- Available tooling: bash, curl, openssl and xxd (usually present by default on modern Linux distributions).
Creating a bucket/container
Optionally, you can use an env.sh file to define your values centrally:
# env.sh
export ACCESS_KEY="YOUR_ACCESS_KEY"
export SECRET_KEY="YOUR_SECRET_KEY"
export REGION="eu-west-1" # e.g. eu-west-1
export ENDPOINT="project-ID.objectstore.eu" # without https:// (host name only)
export BUCKET="my-test-bucket" # lowercase letters/digits/hyphen onlyMake the file readable only for yourself and load the variables into your shell:
chmod 600 env.sh
source env.shWith the script below you create an (empty) bucket via the S3 API. The script automatically builds the required AWS Signature v4 signature using openssl and sends the PUT request with curl. If the bucket already exists, the provider may, depending on the implementation, return either a 2xx or a 409 (see “Response and status codes”).
With env.sh (recommended)
Step 1
Make sure env.sh has been created (see the “Requirements” section) and that you have loaded the variables into your shell:
cd ~/s3-sigv4-demo
source env.sh
Step 2
Create the script create_bucket.sh with the contents below. The script uses the variables from env.sh:
#!/usr/bin/env bash
set -euo pipefail
# Expects ACCESS_KEY, SECRET_KEY, REGION, ENDPOINT and BUCKET
# to be set via: source env.sh
: "${ACCESS_KEY:?Missing ACCESS_KEY}"
: "${SECRET_KEY:?Missing SECRET_KEY}"
: "${REGION:?Missing REGION}"
: "${ENDPOINT:?Missing ENDPOINT}"
: "${BUCKET:?Missing BUCKET}"
SERVICE="s3"
METHOD="PUT"
HOST="${ENDPOINT}"
CANONICAL_URI="/${BUCKET}"
REQUEST_URL="https://${HOST}/${BUCKET}"
# Timestamps
AMZ_DATE="$(date -u +"%Y%m%dT%H%M%SZ")"
DATE_STAMP="$(date -u +"%Y%m%d")"
# 1) Payload (XML) for region (non-us-east-1)
# Note: for us-east-1 you usually need to omit LocationConstraint.
if [[ "${REGION}" == "us-east-1" ]]; then
PAYLOAD=""
else
PAYLOAD="$(cat <<EOF
<CreateBucketConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<LocationConstraint>${REGION}</LocationConstraint>
</CreateBucketConfiguration>
EOF
)"
fi
# 2) Hash payload
PAYLOAD_HASH="$(printf "%s" "${PAYLOAD}" | openssl dgst -sha256 -binary | xxd -p -c 256)"
# 3) Canonical headers + signed headers
if [[ -n "${PAYLOAD}" ]]; then
CANONICAL_HEADERS="content-type:application/xml
host:${HOST}
x-amz-content-sha256:${PAYLOAD_HASH}
x-amz-date:${AMZ_DATE}
"
SIGNED_HEADERS="content-type;host;x-amz-content-sha256;x-amz-date"
else
CANONICAL_HEADERS="host:${HOST}
x-amz-content-sha256:${PAYLOAD_HASH}
x-amz-date:${AMZ_DATE}
"
SIGNED_HEADERS="host;x-amz-content-sha256;x-amz-date"
fi
CANONICAL_REQUEST="${METHOD}
${CANONICAL_URI}
${CANONICAL_HEADERS}
${SIGNED_HEADERS}
${PAYLOAD_HASH}"
CANONICAL_REQUEST_HASH="$(printf "%s" "${CANONICAL_REQUEST}" | openssl dgst -sha256 -binary | xxd -p -c 256)"
CREDENTIAL_SCOPE="${DATE_STAMP}/${REGION}/${SERVICE}/aws4_request"
STRING_TO_SIGN="AWS4-HMAC-SHA256
${AMZ_DATE}
${CREDENTIAL_SCOPE}
${CANONICAL_REQUEST_HASH}"
# Helpers: HMAC-SHA256 -> hex
hmac_sha256_hex_key() {
local key_ascii="$1"
local data="$2"
printf "%s" "${data}" | openssl dgst -sha256 -mac HMAC -macopt "key:${key_ascii}" -binary | xxd -p -c 256
}
hmac_sha256_hex_hexkey() {
local key_hex="$1"
local data="$2"
printf "%s" "${data}" | openssl dgst -sha256 -mac HMAC -macopt "hexkey:${key_hex}" -binary | xxd -p -c 256
}
# 4) Derive signing key (AWS4 + secret)
K_SECRET="AWS4${SECRET_KEY}"
K_DATE="$(hmac_sha256_hex_key "${K_SECRET}" "${DATE_STAMP}")"
K_REGION="$(hmac_sha256_hex_hexkey "${K_DATE}" "${REGION}")"
K_SERVICE="$(hmac_sha256_hex_hexkey "${K_REGION}" "${SERVICE}")"
K_SIGNING="$(hmac_sha256_hex_hexkey "${K_SERVICE}" "aws4_request")"
SIGNATURE="$(hmac_sha256_hex_hexkey "${K_SIGNING}" "${STRING_TO_SIGN}")"
AUTH_HEADER="AWS4-HMAC-SHA256 Credential=${ACCESS_KEY}/${CREDENTIAL_SCOPE}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}"
# 5) Send request
TMP_BODY="$(mktemp)"
if [[ -n "${PAYLOAD}" ]]; then
HTTP_CODE="$(curl -sS -o "${TMP_BODY}" -w "%{http_code}" \
-X "${METHOD}" "${REQUEST_URL}" \
-H "Host: ${HOST}" \
-H "x-amz-date: ${AMZ_DATE}" \
-H "x-amz-content-sha256: ${PAYLOAD_HASH}" \
-H "Content-Type: application/xml" \
-H "Authorization: ${AUTH_HEADER}" \
--data-binary "${PAYLOAD}")"
else
HTTP_CODE="$(curl -sS -o "${TMP_BODY}" -w "%{http_code}" \
-X "${METHOD}" "${REQUEST_URL}" \
-H "Host: ${HOST}" \
-H "x-amz-date: ${AMZ_DATE}" \
-H "x-amz-content-sha256: ${PAYLOAD_HASH}" \
-H "Authorization: ${AUTH_HEADER}" \
--data-binary "")"
fi
echo "HTTP ${HTTP_CODE}"
if [[ "${HTTP_CODE}" =~ ^2 ]]; then
echo "OK: bucket '${BUCKET}' has been created."
rm -f "${TMP_BODY}"
exit 0
fi
echo "ERROR: bucket not created. Response body:"
cat "${TMP_BODY}"
rm -f "${TMP_BODY}"
exit 1
Make the script executable:
chmod +x create_bucket.sh
Step 3
Run the script to create the bucket:
source env.sh
./create_bucket.sh
Explanation:
source env.sh loads your access key, secret key, region, endpoint and bucket name into the shell;
create_bucket.sh automatically builds the AWS Signature v4 signature and sends a PUT request to https://ENDPOINT/BUCKET (path style).
Without env.sh (values in the script)
Step 1
If you don’t want to use a separate env.sh file, you can put the values directly at the top of the script:
#!/usr/bin/env bash
set -euo pipefail
ACCESS_KEY="YOUR_ACCESS_KEY"
SECRET_KEY="YOUR_SECRET_KEY"
REGION="eu-west-1"
ENDPOINT="project-ID.objectstore.eu"
BUCKET="my-test-bucket"
# <rest of create_bucket.sh remains the same>
Then run the script directly:
chmod +x create_bucket.sh
./create_bucket.sh
Response and status codes
- 200 OK (sometimes 204 No Content): the bucket has been created.
- 409 Conflict: the bucket already exists. Depending on the implementation this can mean “already yours” or “name already in use”.
- 403 Forbidden / AccessDenied / SignatureDoesNotMatch: check access key, secret key, endpoint, region and system time (UTC/NTP).
- 400 Bad Request / InvalidBucketName: the bucket name does not comply with the naming rules (lowercase letters, digits and hyphens only).
- 400 Bad Request / InvalidLocationConstraint (or similar): the region/LocationConstraint does not match the endpoint (or the provider expects a different region).
Use the same access key and secret key for other S3 requests as well. The signature follows the same pattern: the payload is hashed, a canonical request and string-to-sign are created, and they are signed with AWS Signature v4 using your secret key.