I have the following configuration in cloudfront
Origins
- Origin name | Origin domain | Origin path | Origin type | Origin Shield region | Origin access
- bucket-user-files.s3.us-east-2.amazonaws.com | bucket-user-files.s3.us-east-2.amazonaws.com | /user-files | S3 | us-east-2 | E29MY3
- bucket-events.s3.us-east-2.amazonaws.com | bucket-events.s3.us-east-2.amazonaws.com | /events | S3 | us-east-2 | E2AYU
in both of them I have activated the Origin Access option: Origin Access Control Settings (recommended)
Behaviors
- Precedence | Path pattern | Origin or origin group | Viewer protocol policy | Cache policy name | Origin request policy name | Response headers policy name
- 0 | events/* | bucket-events.s3.us-east-2.amazonaws.com | Redirect HTTP to HTTPS | TW_CF_S3_UserFIles | TW_CF_S3_UserFIles_Origin | -
- 1 | user-files/* | bucket-user-files.s3.us-east-2.amazonaws.com | Redirect HTTP to HTTPS | TW_CF_S3_UserFIles | TW_CF_S3_UserFIles_Origin | -
- 2 | Default (*) | bucket-user-files.s3.us-east-2.amazonaws.com | Redirect HTTP to HTTPS | TW_CF_S3_UserFIles | TW_CF_S3_UserFIles_Origin | -
in both I have Viewer option active: HTTP methods allowed: GET, HEAD, OPTIONS -> Cache HTTP mwthods: OPTIONS in both in Restrict viewer access, I have it set to yes -> Trusted key groups -> post-files (key group) in both I use the same key group in both in Cache key and origin requests I have selected the option "Cache policy and origin request policy".
in the S3 buckets:
in both buckets I have the same policies only differing in the resource, for their respective bucket.
bucket-user-files
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket-user-files/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::084135377906:distribution/EV633TPE1OT1W"
}
}
}
]
}
bucket-events
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::bucket-events/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::084135377906:distribution/EV633TPE1OT1W"
}
}
}
]
}
in both buckets I have the following configuration Block public access (bucket configuration) Block all public access Disabled
- Not selected: Block public access to buckets and objects granted through new access control lists (ACL)
- Not selected : Block public access to buckets and objects granted through any Access Control List (ACL)
- selected: Block public access to buckets and objects granted through new bucket policies and public access points
- selected: Block public and inter-account access to buckets and objects granted through any public bucket and access point policy.
Access Control List (ACL)
Recipient | Objects | Bucket ACLs
Bucket owner (your AWS account) | List, Read, Write | Read, Write
Everyone (public access) | - | - | -
Group of authenticated users (anyone with an AWS account) | - | - | -
Group S3 log forwarding | - | - | -
Cross-Origin Resource Sharing (CORS)
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"GET"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
now, in my code I have the following
<?php
namespace App\Service\AWS;
use Aws\CloudFront\CloudFrontClient;
use Aws\Exception\AwsException;
use DateInterval;
use DateTime;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\Cache\TagAwareCacheInterface;
class CloudFront
{
const EXPIRATION_TIME_CLOUDFRONT_URL = 'PT8H';
const CACHE_PUBLIC_URL_TIME = 'P2M';
private CloudFrontClient $cloudFrontClient;
private TagAwareCacheInterface $cache;
private string $keyPath;
private string $keyId;
private string $distributionUrl;
private string $origin;
public function __construct(
CloudFrontClient $cloudFrontClient,
TagAwareCacheInterface $cache,
string $PRIVATE_KEY_ID,
string $PRIVATE_KEY_PATH,
string $DISTRIBUTION_URL,
) {
$this->cloudFrontClient = $cloudFrontClient;
$this->cache = $cache;
$this->keyId = $PRIVATE_KEY_ID;
$this->keyPath = $PRIVATE_KEY_PATH;
$this->distributionUrl = $DISTRIBUTION_URL;
}
public function getSignedUrl(string $origin, string $key, string $ip): string {
$expires = new DateTime();
$expires->add(new DateInterval(self::EXPIRATION_TIME_CLOUDFRONT_URL));
if ($ip == "::1" || $ip == "127.0.0.1") { // for test in local
$ip = file_get_contents('https://ipecho.net/plain');
}
$resource = $this->distributionUrl.$origin."/".$key;
$customPolicy = <<<POLICY
{
"Statement": [
{
"Resource": "{$resource}",
"IpAddress": {"AWS:SourceIp": "{$ip}/32"},
"Condition": {
"DateLessThan": {"AWS:EpochTime": {$expires->getTimestamp()}}
}
}
]
}
POLICY;
try {
return $this->cloudFrontClient->getSignedUrl([
'url' => $resource,
'policy' => $customPolicy,
'private_key' => $this->keyPath,
'key_pair_id' => $this->keyId
]);
} catch (AwsException $e) {
return 'Error: ' . $e->getAwsErrorMessage();
}
}
/**
* @throws \Psr\Cache\InvalidArgumentException
*/
public function getPublicUrl(string $origin, string $key)
{
$itemName = htmlspecialchars($key);
$itemName = str_replace('/','',$itemName);
return $this->cache->get("public_".$itemName."_file",
function (ItemInterface $item) use ($origin, $key) {
$cachedTime = new DateInterval(self::CACHE_PUBLIC_URL_TIME);
$item->expiresAfter($cachedTime);
$item->tag(['public-url-files']);
$expires = new DateTime();
$expires->add($cachedTime);
try {
return $this->cloudFrontClient->getSignedUrl([
'url' => $this->distributionUrl.$origin."/".$key,
'expires' => $expires->getTimestamp(),
'private_key' => $this->keyPath,
'key_pair_id' => $this->keyId
]);
} catch (AwsException $e) {
return 'Error: ' . $e->getAwsErrorMessage();
}
}
);
}
}
when I call getSignedUrl() when executing the return $this->cloudFrontClient->getSignedUrl I pass the parameters:
url => https://d1andaiyzi47n1.cloudfront.net/user-files/e230a742-e5d5-4c51-ac76-32dc1ceadb91/post/838e9873-0c65-4525-a563-87a0cfb8b283/64f4e586a3cae4.40728513.png
policy => {
"Statement": [
{
"Resource": "https://d1andaiyzi46n1.cloudfront.net/user-files/e230a742-e5d5-4c51-ac76-32dc1ceadb91/post/838e9873-0c65-4525-a563-87a0cfb8b283/64f4e586a3cae4.40728513.png",
"IpAddress": {"AWS:SourceIp": "***."***.."***.."***./32"},
"Condition": {
"DateLessThan": {"AWS:EpochTime": 1693799943}
}
}
]
}
at the end it does return a url that appears to be signed correctly e.g.
I see this url in the time limit that I set as well as from the ip that I set. however when I open that url signed in the browser I get the following message
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<Error>
<Code>AccessDenied</Code>
<Message>Access Denied</Message>
<RequestId>5EAYW06QE5218T9T</RequestId>
<HostId>smMq546FKNMqAUNepRJu/bU1FNY3oL9OQtR28JE0UcqLQir5uYVjyMDHsstnpLrU0tHsTPU/N48=</HostId>
</Error>
I am also doing the manual test from the aws cli and the same thing happens, it says access denied.
here is the aws command
aws cloudfront sign --url 'https://d1andaiyzi46n1.cloudfront.net/user-files/e230a742-e5d5-4c51-ac76-32dc1ceadb91/post/838e9873-0c65-4525-a563-87a0cfb8b283/64f4e586a3cae4.40728513.png' --key-pair-id 'K2Z8L65Y2UCBGT' --private-key file://C:/Users/.../secrets/cloudfront-post-files-key1_private.pem --date-less-than '2024-09-04T12:00:00Z'
The logs that cloudfront generates do not give much information only that it seems to be some configuration or permissions issue, but it does not give any indication of what it could be. This is supported by the fact that as soon as the request arrives to cloudfront immediately launches the log with the error, without having a reasonable waiting time to give rise to another diagnosis.
While working with AWS CloudFront having multiple origins, I encountered an "Access Denied" issue. After some investigation, I found the root cause and a solution.
Problem:
I mistakenly believed that by defining a unique path pattern (Path Pattern) for each behavior according to the bucket, and then constructing the CloudFront request URL with that path pattern, CloudFront would automatically route to the appropriate origin. Unfortunately, it doesn't work that way.
Even if you define a distinct path pattern for each origin, when the constructed CloudFront URL is accessed, it attempts to fetch the file from the S3 bucket, including the path pattern itself. For example, I was constructing URLs that looked like this: https://cloudfront-distribution/user-files/[file-key] I assumed that CloudFront would use the user-files/* path pattern to route to the correct S3 bucket origin. However, in reality, it was looking for a folder named user-files/ within the S3 bucket. My S3 bucket didn't have a folder named user-files/. Instead, it contained several folders named by user UUIDs.
Solution:
Understanding the issue led me to the conclusion that the path patterns aren't to be used as mere routing indicators in CloudFront. Instead, they should represent the actual structure within the S3 bucket.
Given that I had user-specific UUID folders within the S3 bucket, it wasn't feasible to define a path pattern for each of them. The most straightforward solution was to create a separate CloudFront distribution for each S3 bucket, avoiding the need for additional path patterns.
In conclusion, if you're facing a similar "Access Denied" issue and you're using CloudFront with multiple origins and path patterns, double-check how you're constructing your URLs. Ensure that the path patterns align with the actual structure of your S3 bucket, or consider using separate distributions for clarity and simplicity.
I hope this helps anyone facing a similar issue!