Ravindra BagaleCourses & study guides

16. Live Project: Building a Reels App with EC2, S3 and RDS

16.5 Configuration and Shared Code

config.php web root chya baher aahe – browser kadhi ti file maagu shakat nahi. Tyat AWS access key nahi, fakt bucket che naav aani region.

config.php

<?php
// /var/www/reels/config.php  – lives OUTSIDE the web root (public/).
// Permissions: sudo chown root:apache config.php && sudo chmod 640 config.php
//              (Ubuntu: root:www-data)
// No AWS access keys here: the SDK gets temporary credentials from the EC2 IAM role.
return [
    'db_host'         => 'reels-db.abcdefgh1234.ap-south-1.rds.amazonaws.com',
    'db_port'         => 3306,
    'db_name'         => 'reelsdb',
    'db_user'         => 'reels_app',
    'db_pass'         => 'Use-A-Long-Random-Password-Here!',
    'db_ssl_ca'       => '/etc/pki/rds/global-bundle.pem',   // RDS CA bundle ('' = no TLS)
    'aws_region'      => 'ap-south-1',
    's3_bucket'       => 'ravindra-reels-media-pune',
    's3_prefix'       => 'videos/',
    'max_video_bytes' => 50 * 1024 * 1024,                   // 50 MB
    'url_ttl'         => '+20 minutes',                      // presigned URL lifetime
    'cookie_secure'   => true,                               // false ONLY for plain-http tests
];
sudo mkdir -p /etc/pki/rds
sudo curl -o /etc/pki/rds/global-bundle.pem https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem
sudo chown root:apache /var/www/reels/config.php && sudo chmod 640 /var/www/reels/config.php   # Ubuntu: root:www-data

src/bootstrap.php is included by every page. It starts a hardened session, opens PDO to RDS with real prepared statements and TLS, creates the S3 client without any credentials (the SDK finds the IAM role automatically), and provides small helpers: e() for HTML escaping, CSRF tokens, require_login() and json_out().

src/bootstrap.php

<?php
// src/bootstrap.php – shared by every page: config, session, DB, S3 and helpers
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Aws\S3\S3Client;

$CONFIG = require __DIR__ . '/../config.php';

function cfg(string $key): mixed
{
    global $CONFIG;
    return $CONFIG[$key];
}

// ---------- session: HttpOnly, Secure, SameSite cookies ----------
session_name('REELSSESSID');
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'secure'   => cfg('cookie_secure'),
    'httponly' => true,
    'samesite' => 'Lax',
]);
ini_set('session.use_strict_mode', '1');
session_start();

// ---------- database (RDS MySQL through PDO) ----------
function db(): PDO
{
    static $pdo = null;
    if ($pdo === null) {
        $dsn  = sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
                        cfg('db_host'), cfg('db_port'), cfg('db_name'));
        $opts = [
            PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES   => false,          // real prepared statements
        ];
        if (cfg('db_ssl_ca') !== '') {
            $opts[PDO::MYSQL_ATTR_SSL_CA] = cfg('db_ssl_ca');  // TLS to RDS
        }
        $pdo = new PDO($dsn, cfg('db_user'), cfg('db_pass'), $opts);
    }
    return $pdo;
}

// ---------- S3 client: NO keys – credentials come from the EC2 IAM role ----------
function s3(): S3Client
{
    static $client = null;
    return $client ??= new S3Client([
        'version' => 'latest',
        'region'  => cfg('aws_region'),
    ]);
}

function presigned_url(string $key): string
{
    $cmd = s3()->getCommand('GetObject', ['Bucket' => cfg('s3_bucket'), 'Key' => $key]);
    return (string) s3()->createPresignedRequest($cmd, cfg('url_ttl'))->getUri();
}

// ---------- helpers ----------
function e(?string $s): string            // escape for HTML output (stops XSS)
{
    return htmlspecialchars($s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function csrf_token(): string
{
    if (empty($_SESSION['csrf'])) {
        $_SESSION['csrf'] = bin2hex(random_bytes(32));
    }
    return $_SESSION['csrf'];
}

function csrf_check(): void
{
    $sent = $_POST['csrf'] ?? ($_SERVER['HTTP_X_CSRF_TOKEN'] ?? '');
    if (!is_string($sent) || !hash_equals(csrf_token(), $sent)) {
        http_response_code(403);
        exit('Invalid CSRF token');
    }
}

function current_user(): ?array
{
    if (empty($_SESSION['uid'])) {
        return null;
    }
    return ['id' => (int) $_SESSION['uid'], 'username' => (string) $_SESSION['uname']];
}

function require_login(bool $json = false): array
{
    $u = current_user();
    if ($u === null) {
        if ($json) {
            json_out(['error' => 'login required'], 401);
        }
        header('Location: /login.php');
        exit;
    }
    return $u;
}

function json_out(array $data, int $status = 200): never
{
    http_response_code($status);
    header('Content-Type: application/json; charset=utf-8');
    header('Cache-Control: no-store');
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
    exit;
}

function page_top(string $title): void
{
    echo '<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">'
       . '<meta name="viewport" content="width=device-width, initial-scale=1">'
       . '<title>' . e($title) . ' · Mitrano Reels</title>'
       . '<link rel="stylesheet" href="/assets/reels.css"></head><body class="page">'
       . '<div class="box"><h1>' . e($title) . '</h1>';
}

function page_bottom(): void
{
    echo '</div></body></html>';
}
Helper Protects against
e() = htmlspecialchars Stored/reflected XSS in HTML pages
csrf_token() / csrf_check() with hash_equals CSRF – other sites submitting forms as your user
PDO::​ATTR_​EMULATE_​PREPARES => false SQL injection (true server-side prepared statements)
session_​set_​cookie_​params (HttpOnly, Secure, SameSite) Cookie theft by JavaScript, cookies over HTTP, cross-site requests
S3Client without keys Leaked access keys – there are none to leak

Why this matters for security

The SDK's default credential chain checks environment variables, then ~/.aws/credentials, then the EC2 instance role. Keeping the code free of keys means a leaked Git repository or a readable config.php does not expose your AWS account. Temporary role credentials rotate automatically.

Ravindra Bagale's Tip

For testing, students set cookie_secure to false and forget to set it back to true after adding HTTPS. Set it to true right after Certbot, and check in browser DevTools → Application → Cookies that Secure and HttpOnly are ticked. And never copy config.php into public/!

Practice task

Create config.php and src/bootstrap.php, set the permissions shown, and run php -l src/bootstrap.php. Then run sudo -u apache php -r 'var_dump(is_readable("/var/www/reels/config.php"));' (Ubuntu: www-data) to prove PHP-FPM can read it.