AWS S3 bucket origin (private)

Use AWS authenticated requests (signature version 4) to protect communication between your Fastly service and AWS.

VCL

Use this solution in your VCL service (click RUN below to test this solution or clone it to make changes):

Compute@Edge

Use this solution in your Compute@Edge service:

  1. Rust
/// S3 Backend Name
/// The backend domain name can be set as s3.BUCKET_REGION.amazonaws.com
/// For other options check https://docs.aws.amazon.com/general/latest/gr/s3.html
const BACKEND_S3: &str = "my-s3-backend";
// Hash of empty string, used for authentication string generation
// caculated from hex::encode(Hash::hash("".as_bytes()));
const EMPTY_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
/// Google Cloud Storage Configuration for Interoperability API
struct S3Config {
access_key_id: String,
secret_access_key: String,
region: String,
bucket: String,
}
impl S3Config {
/// Load the GCS configuration.
///
/// This assumes an Edge Dictionary named "s3_config" is attached to this service,
/// with entries for access_key_id, secret_key, region and bucket.
fn load_config() -> Self {
let dict = Dictionary::open("s3_config");
Self {
access_key_id: dict.get("access_key_id").expect("access key id configured"),
secret_access_key: dict
.get("secret_access_key")
.expect("secret access key configured"),
region: dict.get("region").expect("region configured"),
bucket: dict.get("bucket").expect("bucket configured"),
}
}
}
#[fastly::main]
fn main(mut req: Request) -> Result<Response, Error> {
log_fastly::init_simple("my_log", log::LevelFilter::Info);
fastly::log::set_panic_endpoint("my_log")?;
// Only generate authorize header for GET and HEAD request
// Pass PURGE to backend as it is
// Block other kinds of method
let resp = match req.get_method() {
&Method::GET | &Method::HEAD => {
let s3_config = S3Config::load_config();
req = authorize_s3_request(
req,
&s3_config.access_key_id,
&s3_config.secret_access_key,
&s3_config.region,
&s3_config.bucket,
)?;
req.send(BACKEND_S3)?
}
m if m == "PURGE" => {
// url of cached object has no query string
// so when do the purge, we need to make sure the url matches
req.remove_query();
req.send(BACKEND_S3)?
}
_ => Response::from_status(StatusCode::METHOD_NOT_ALLOWED),
};
Ok(resp)
}
/// Generate HMAC hash of message with key
fn sign(key: &[u8], message: &str) -> [u8; 32] {
HMAC::mac(message.as_bytes(), key)
}
/// Generate Authorization headers of the S3 request
fn authorize_s3_request(
mut req: Request,
access_key_id: &str,
secret_access_key: &str,
region: &str,
bucket: &str,
) -> Result<Request> {
// Ignore the query string from client
req.remove_query();
let canonical_querystring = "";
let x_amz_content_sha256 = EMPTY_HASH;
let amz_date = chrono::Utc::now().format("%Y%m%dT%H%M%SZ").to_string();
// Prepare authentication related information
// For details, please refer https://developer.fastly.com/learning/integrations/backends/
let host = format!("{}.s3.{}.amazonaws.com", bucket, region);
// No need to URL encode twice for S3 object
// https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
// "Each path segment must be URI-encoded twice (EXCEPT for Amazon S3 which only gets URI-encoded once)"
// Do url decode and re-encode in case some client are not do url encoding according S3 spec
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
let path_url_decoded = urlencoding::decode(req.get_path())?;
let path_url_encoded = urlencoding::encode(&path_url_decoded);
// No url encode of "/" in object id
let canonical_uri = path_url_encoded.replace("%2F", "/");
// Generate Authorization header value
// Force the method to GET
let authorization_value = generate_s3_authorization_header(
access_key_id,
secret_access_key,
region,
&host,
&canonical_uri,
canonical_querystring,
x_amz_content_sha256,
"GET",
&amz_date,
"s3",
);
req.set_method(Method::GET);
// Add authorization related headers to request
req.set_header(header::HOST, &host);
req.set_header(header::AUTHORIZATION, &authorization_value);
req.set_header("x-amz-content-sha256", x_amz_content_sha256);
req.set_header("x-amz-date", &amz_date);
log::info!(
"Path: {}, Host: {}, x-amz-date: {}, Authorization: {}",
req.get_path(),
host,
amz_date,
authorization_value,
);
Ok(req)
}
/// Generate authoriation header for S3 object access
/// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
fn generate_s3_authorization_header(
access_key_id: &str,
secret_access_key: &str,
region: &str,
host: &str,
canonical_uri: &str,
canonical_querystring: &str,
x_amz_content_sha256: &str,
method: &str,
amz_date: &str,
service: &str,
) -> String {
let canonical_headers = format!(
"host:{}\nx-amz-content-sha256:{}\nx-amz-date:{}\n",
host, x_amz_content_sha256, amz_date
);
let signed_headers = "host;x-amz-content-sha256;x-amz-date";
let canonical_request = format!(
"{}\n{}\n{}\n{}\n{}\n{}",
method,
canonical_uri,
canonical_querystring,
canonical_headers,
signed_headers,
x_amz_content_sha256
);
// credential_date format is YYYYMMDD
let credential_date = &amz_date[..8];
let credential_scope = format!("{}/{}/{}/aws4_request", credential_date, region, service);
// Compose string to be signed
let canonical_request_hash = hex::encode(Hash::hash(canonical_request.as_bytes()));
let string_to_sign = format!(
"AWS4-HMAC-SHA256\n{}\n{}\n{}",
amz_date, credential_scope, canonical_request_hash
);
// Generate signing key
let aws4_secret = format!("AWS4{}", secret_access_key).into_bytes();
let date_key = sign(&aws4_secret, credential_date);
let region_key = sign(&date_key, region);
let service_key = sign(&region_key, service);
let signing_key = sign(&service_key, "aws4_request");
// Generate signature
let signature = hex::encode(sign(&signing_key, &string_to_sign));
// Compose authorization header value
format!(
"AWS4-HMAC-SHA256 Credential={}/{},SignedHeaders={},Signature={}",
access_key_id, credential_scope, signed_headers, signature
)
}