Debug School

rakesh kumar
rakesh kumar

Posted on

How to Store Multi-Tenant Application Videos Securely on AWS S3 Using EC2 IAM Roles

Purpose
Step-by-step implementation
PROMPT

Purpose

He wants all videos moved from the EC2 server’s local disk to one common private AWS S3 bucket.

The purpose is to:

Reduce EC2 disk usage.
Prevent the EC2 disk from becoming full.
Store large numbers of videos reliably.
Keep every organization’s videos separated.
Organize videos by albums.
Secure videos from unauthorized access.
Preserve videos even when EC2 is restarted or replaced.
Store each uploaded video’s S3 location in the database.
Enter fullscreen mode Exit fullscreen mode

Required structure:

S3 bucket: videos.wizbrand.com
└── organizations
    └── {organization_id}
        └── albums
            └── {album_id}
                └── {video_file}
Enter fullscreen mode Exit fullscreen mode

Example:

videos.wizbrand.com
└── organizations
    └── mymedicplus
        └── albums
            ├── rajesh
            │   ├── video1.mp4
            │   └── video2.mp4
            └── rajesh1
                └── video3.mp4
Enter fullscreen mode Exit fullscreen mode

Step-by-step implementation

Step 1: Verify the S3 bucket

Confirm that this bucket already exists:

videos.wizbrand.com
Enter fullscreen mode Exit fullscreen mode

Also confirm its AWS region, for example:

ap-south-1
Enter fullscreen mode Exit fullscreen mode

Keep S3 Block Public Access enabled because the videos should remain private.

Step 2: Create an IAM role for EC2

Create an IAM role:

WizBrandVideosEC2Role
Enter fullscreen mode Exit fullscreen mode

Select:

Trusted entity: AWS service
Service: EC2
Enter fullscreen mode Exit fullscreen mode

This role allows the application running on EC2 to access S3 without storing permanent AWS access keys.

Step 3: Add S3 permissions to the IAM role

Attach this custom policy to the role:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListVideoBucket",
      "Effect": "Allow",
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::videos.wizbrand.com",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "organizations",
            "organizations/*"
          ]
        }
      }
    },
    {
      "Sid": "ManageVideoObjects",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::videos.wizbrand.com/organizations/*"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

This allows the EC2 application to:

Upload videos.
Read videos.
Delete videos.
List uploaded videos.
Perform multipart uploads for large videos.
Step 4: Attach the role to EC2

Open:

AWS Console
→ EC2
→ Instances
→ Select your instance
→ Actions
→ Security
→ Modify IAM role

Attach:

WizBrandVideosEC2Role

If EC2 already has an IAM role, add the S3 policy to that existing role instead of replacing it unnecessarily.

Step 5: Apply a bucket policy

Replace the AWS account ID and role name:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowWizBrandEC2Role",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/WizBrandVideosEC2Role"
      },
      "Action": [
        "s3:ListBucket",
        "s3:GetBucketLocation"
      ],
      "Resource": "arn:aws:s3:::videos.wizbrand.com",
      "Condition": {
        "StringLike": {
          "s3:prefix": [
            "organizations",
            "organizations/*"
          ]
        }
      }
    },
    {
      "Sid": "AllowVideoObjectOperations",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/WizBrandVideosEC2Role"
      },
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::videos.wizbrand.com/organizations/*"
    },
    {
      "Sid": "DenyInsecureConnection",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::videos.wizbrand.com",
        "arn:aws:s3:::videos.wizbrand.com/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    }
  ]
}

Enter fullscreen mode Exit fullscreen mode

Apply it at:

S3

→ videos.wizbrand.com
→ Permissions
→ Bucket policy
Enter fullscreen mode Exit fullscreen mode

Step 6: Configure Laravel .env

Add:

FILESYSTEM_DISK=s3

AWS_DEFAULT_REGION=ap-south-1
AWS_BUCKET=videos.wizbrand.com
AWS_USE_PATH_STYLE_ENDPOINT=false
AWS_URL=
AWS_ENDPOINT=
Enter fullscreen mode Exit fullscreen mode

When using the EC2 IAM role, do not add permanent credentials:


AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
Enter fullscreen mode Exit fullscreen mode

The AWS SDK automatically obtains temporary credentials from the EC2 IAM role.

Clear Laravel configuration cache:

/opt/lampp/bin/php artisan optimize:clear
Enter fullscreen mode Exit fullscreen mode

Step 7: Create the S3 object path

When a video is uploaded, the backend should generate:

$path = "organizations/{$organization->id}/albums/{$album->id}";
Enter fullscreen mode Exit fullscreen mode

Generate a unique filename:

$filename = (string) Str::uuid() . '.' . $file->getClientOriginalExtension();
Enter fullscreen mode Exit fullscreen mode

Complete object key:

$objectKey = "{$path}/{$filename}";
Enter fullscreen mode Exit fullscreen mode

Example:

organizations/25/albums/89/550e8400-e29b-41d4-a716.mp4
Enter fullscreen mode Exit fullscreen mode

S3 automatically presents prefixes as folders. You do not need to create empty folders beforehand.

Step 8: Validate organization and album ownership

Before uploading, verify:

$album = Album::where('id', $albumId)
    ->where('organization_id', $currentUser->organization_id)
    ->firstOrFail();
Enter fullscreen mode Exit fullscreen mode

Never accept organization_id directly from the upload request without authorization.

Use the authenticated user’s organization:

$organizationId = auth()->user()->organization_id;
Enter fullscreen mode Exit fullscreen mode

This prevents users from uploading into another organization’s folder.

Step 9: Upload the video to S3

Laravel example:

use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;

$organizationId = auth()->user()->organization_id;

$album = Album::where('id', $request->album_id)
    ->where('organization_id', $organizationId)
    ->firstOrFail();

$file = $request->file('video');

$filename = Str::uuid() . '.' . $file->getClientOriginalExtension();

$objectKey = "organizations/{$organizationId}/albums/{$album->id}/{$filename}";

Storage::disk('s3')->putFileAs(
    "organizations/{$organizationId}/albums/{$album->id}",
    $file,
    $filename,
    ['visibility' => 'private']
);
Enter fullscreen mode Exit fullscreen mode

Step 10: Store video information in the database

Recommended database columns:

id
organization_id
album_id
disk
bucket
object_key
original_name
mime_type
file_size
created_by
created_at
Enter fullscreen mode Exit fullscreen mode

Example record:

organization_id: 25
album_id: 89
disk: s3
bucket: videos.wizbrand.com
object_key: organizations/25/albums/89/uuid-video.mp4
original_name: introduction.mp4
Enter fullscreen mode Exit fullscreen mode

Example:

Video::create([
    'organization_id' => $organizationId,
    'album_id'        => $album->id,
    'disk'            => 's3',
    'bucket'          => config('filesystems.disks.s3.bucket'),
    'object_key'      => $objectKey,
    'original_name'   => $file->getClientOriginalName(),
    'mime_type'       => $file->getMimeType(),
    'file_size'       => $file->getSize(),
    'created_by'      => auth()->id(),
]);
Enter fullscreen mode Exit fullscreen mode

Store the stable object_key in the database. Do not store an expiring presigned URL as the permanent video URL.

Step 11: Generate a temporary viewing URL

Because the bucket is private:

$url = Storage::disk('s3')->temporaryUrl(
    $video->object_key,
    now()->addMinutes(30)
);
Enter fullscreen mode Exit fullscreen mode

This URL expires after 30 minutes.

Alternatively, use CloudFront with signed URLs when video traffic becomes large.

Step 12: Handle album creation

Creating an album should primarily create a database record:

albums
├── id
├── organization_id
├── name
├── created_by
└── timestamps
Enter fullscreen mode Exit fullscreen mode

You do not have to create an empty S3 folder. The album prefix appears automatically when its first video is uploaded:

organizations/{organization_id}/albums/{album_id}/{video}
Enter fullscreen mode Exit fullscreen mode

Step 13: Handle video deletion

Verify ownership before deleting:

$video = Video::where('id', $videoId)
    ->where('organization_id', auth()->user()->organization_id)
    ->firstOrFail();

Storage::disk('s3')->delete($video->object_key);

$video->delete();
Enter fullscreen mode Exit fullscreen mode

Delete the S3 object first, then remove or soft-delete its database record.

Step 14: Test the complete workflow

Test these cases:

User can upload a video to their organization and album.
The object appears in the correct S3 prefix.
The object key is stored in the database.
Authorized users can watch the video.
Unauthorized users cannot access it.
A user cannot upload to another organization.
Video deletion removes both the S3 object and database record.
Large multipart video uploads work correctly.
EC2 does not retain unnecessary temporary video files.

Final architecture:

User
  ↓
videos.wizbrand.com
  ↓
Authenticate and identify organization
  ↓
Validate album ownership
  ↓
Upload using EC2 IAM role
  ↓
Private S3 bucket
  ↓
organizations/{org-id}/albums/{album-id}/{video}
  ↓
Store S3 object key in database
  ↓
Generate temporary URL for authorized viewing
Enter fullscreen mode Exit fullscreen mode

PROMPT

Requirement of use of videos storage to aws s3 as below for videos.wizbrand.com;

  • Mind it - This server is aws ec2 already.
  • For videos.wizbrand.com, there is one S3 Bucket name is configured in .env file. I.e videos.wizbrand.com
  • That bucket, You must used to create org specific folder inside a bucket. I.e, for each org there would be 1 folder dedicated.
  • Each videos uploaded by any users as part of that org, must should be uploaded to their dedicated folder in the common s3 bucket.
  • EACH ALBUM CREATED By users would become a folder under org.
  • Files would belong to album == respective folder of that org.

- each file uploaded to s3 bucket via videos.wizbrand.com, its Object URL must be stored in db.

Eg.
Org name - mymedicplus
Album name - rajesh
Album name - rajesh1

Q1 - Now research requirement and suggest me the Bucket policy which is need for this bucket to be applied on S3.
Q2 - Also, Do we need to set any IAM permission for this as well? Or Do we need to set any IAM roles for it as part of requirement. Plz suggest me.

Top comments (0)