THU.JUN.18
2026
23:37:13
← back to modules MODULE · 03 · HOMESTREAM
0 / 10 chapters complete · 0%

Lock It Down — Login + LAN-Only

HomeStream is fun but right now anyone on your WiFi (kids, guests, that one neighbor whose router signal is strong enough) can browse and stream. Two layers of defense: a session-based login wall, and a firewall rule that restricts the whole app to your local subnet. Both are cheap, both are smart, both reuse exactly what you built in Part 2.

A single-user login (no registration page — you set the password by hand). A require_login() guard at the top of every page. A ufw rule restricting access to your LAN subnet only. Stream requests stay protected because they go through the same auth check.

The threat model

Let's be honest about who we're defending against. Your home server isn't a juicy target for nation-states; it's exposed to your home WiFi network. Realistic threats are: kids exploring, guests being curious, a misconfigured router accidentally exposing port 80 to the internet, a roommate stumbling into HomeStream and getting freaked out by your weird playlists. So our security goals are modest: require a password to use the app, and ensure the app refuses requests from outside the local network. Both straightforward.

If you ever DO expose HomeStream to the open internet (port-forwarding, dynamic DNS, etc.), you'd want more — rate limiting, real HTTPS via Let's Encrypt, maybe 2FA. We're not going there in this chapter. The LAN-only design is the bigger security win.

Single-user simplification

Part 2's MedTrack had a users table because each user had their own meds. HomeStream is different — it's YOUR library. There's no concept of "user A's movies vs user B's movies." We could still have multiple user accounts (the family shares it), but I'm going to keep this chapter to single-user for clarity. The login is "the password to access HomeStream." One credential, configured by you, used by everyone in the house.

If you later want multi-user (so play history is per-user, or you want to lock certain content), it's a small lift — same users table from MedTrack, plus a users.role column for admin.

The password — set once, never check in to git

We don't even need a database row for the password. We store the hash in a single config file outside the web root. The login form checks the submitted password against the stored hash with password_verify. Done.

Why no database? Because adding a users table for one row is overkill, and a tiny config file is easier to manage when you want to change the password (edit the file). It's also one fewer SQL injection surface.

The require_auth() guard

Same pattern as Part 2. One include at the top of every public page checks for a valid session, and bounces to the login page if not. The streaming endpoint gets the same guard, because we definitely don't want random LAN visitors hotlinking media files.

LAN-only via Apache or ufw

Two ways to restrict access to the local subnet. Apache way is to add a Require ip 192.168.0.0/24 in the vhost config — declarative, lives with the site config. ufw way is to drop external connections at the firewall level — system-wide, blunter. We'll use the Apache way because it's per-site (your other vhosts can stay open) and it doesn't break SSH from outside the LAN (good if you ever ssh in from a hotspot).

Build: HomeStream Auth

Going to put all three pieces together: a password config, a login flow, a require_auth helper, and an Apache restriction. By the end, hitting HomeStream from outside your LAN gets denied at the door, and even on your LAN you need to log in before anything works.

  1. Make a config folder outside the web root: mkdir /home/erictey/server/homestream/config
  2. Generate a password hash. From the CLI on Lubuntu:
    php -r "echo password_hash('your-strong-password', PASSWORD_DEFAULT), PHP_EOL;"
    It prints a long string like $2y$10$.... Copy it.
  3. Create /home/erictey/server/homestream/config/auth.php:
    <?php
    return [
        'password_hash' => '$2y$10$YOUR_HASH_HERE',
    ];
  4. Create /home/erictey/server/homestream/lib/auth.php:
    <?php
    declare(strict_types=1);
    
    function auth_config(): array {
        return require __DIR__ . '/../config/auth.php';
    }
    
    function start_session_once(): void {
        if (session_status() !== PHP_SESSION_ACTIVE) {
            session_start();
        }
    }
    
    function login(string $password): bool {
        $cfg = auth_config();
        if (!password_verify($password, $cfg['password_hash'])) {
            return false;
        }
        start_session_once();
        session_regenerate_id(true);
        $_SESSION['authed'] = true;
        return true;
    }
    
    function logout(): void {
        start_session_once();
        $_SESSION = [];
        if (ini_get('session.use_cookies')) {
            $p = session_get_cookie_params();
            setcookie(session_name(), '', time() - 42000,
                $p['path'], $p['domain'], $p['secure'], $p['httponly']);
        }
        session_destroy();
    }
    
    function require_auth(): void {
        start_session_once();
        if (empty($_SESSION['authed'])) {
            header('Location: /server/homestream/public/login.php');
            exit;
        }
    }
  5. Create /home/erictey/server/homestream/public/login.php:
    <?php
    declare(strict_types=1);
    require __DIR__ . '/../lib/auth.php';
    
    $error = null;
    if ($_SERVER['REQUEST_METHOD'] === 'POST') {
        if (login((string)($_POST['password'] ?? ''))) {
            header('Location: index.php');
            exit;
        }
        $error = 'Wrong password.';
    }
    ?>
    <!DOCTYPE html>
    <html><body style="background:#07050d;color:#f0e9ff;font-family:monospace;padding:30px;max-width:400px;margin:auto">
      <h1 style="color:#ff2e88">HomeStream</h1>
      <?php if ($error): ?>
        <p style="color:#ff4e6a"><?= htmlspecialchars($error) ?></p>
      <?php endif; ?>
      <form method="post">
        <input name="password" type="password" autofocus required
               style="display:block;width:100%;padding:8px;margin:8px 0;background:#110a1c;border:1px solid #ff2e88;color:#f0e9ff;font-family:monospace">
        <button style="background:#ff2e88;border:none;color:#07050d;padding:8px 20px;cursor:pointer;font-family:monospace">Enter</button>
      </form>
    </body></html>
  6. Create /home/erictey/server/homestream/public/logout.php:
    <?php
    require __DIR__ . '/../lib/auth.php';
    logout();
    header('Location: login.php');
    exit;
  7. Now lock down the existing pages. At the top of public/index.php, public/play.php, AND public/stream.php, add right after the requires:
    require __DIR__ . '/../lib/auth.php';
    require_auth();
  8. LAN-only restriction. Edit your Apache vhost config (e.g. /etc/apache2/sites-available/000-default.conf), add a Directory block:
    <Directory /home/erictey/server/homestream/public>
        Require ip 192.168.0.0/24
    </Directory>
    Then sudo apache2ctl -t && sudo systemctl reload apache2.
  9. Test:
    • From your LAN, visit any HomeStream page. Should bounce to login.
    • Enter the password. Should land on the library.
    • Visit logout.php — should kick you back to login.
    • If you have a way to test from outside the LAN (a phone hotspot for example), you should get 403 Forbidden — the IP restriction is working.

Stretch goals:

  • Add a CSRF token to the login form (Part 2 has the pattern).
  • Rate-limit failed logins — store fail count in a small SQLite or just $_SESSION, block after 5 attempts for 60 seconds.
  • Add a "remember me for 30 days" checkbox that sets a longer cookie lifetime.
  • Make it multi-user by reusing the users table from MedTrack — useful if family members want their own play history.

What you flexed: password_hash and password_verify, session_regenerate_id to prevent fixation, the require_auth guard pattern across multiple pages, the config-file-outside-web-root pattern for credentials, and Apache's Require ip directive for network-level restriction. Two layers of defense — login + LAN-only — for a personal-scale threat model.