Skip to content
prod e051e98
Browse

6 · Commit & secure

Objective — make the working install durable and safe: commit the installer’s output (especially the storage/installed marker), document any author patches in the shipped vendor tree, verify deploy symlinks and shared_files, then lock down /install and /update with layered defense.

The app works — now make the state durable and safe. This page commits the installer’s artifacts, records vendor patches that would break on composer update, confirms the deploy symlinks survive a release, and slams the installer routes shut.

See the installer’s footprint before staging anything.

  1. Check the working tree.

    Terminal window
    git status
    # Expected: modified storage/**/.gitignore, new storage/installed, maybe lock files + public/build/
    • ✅ The installer’s modified + new files are visible.
  2. Backfill any missing storage/** ignore files (version-aware — Laravel 11+ and debugbar add new subdirs).

    Terminal window
    for dir in storage/app storage/app/public storage/app/private \
    storage/framework storage/framework/cache storage/framework/cache/data \
    storage/framework/sessions storage/framework/testing storage/framework/views \
    storage/logs storage/debugbar; do
    [ -d "$dir" ] && [ ! -f "$dir/.gitignore" ] && printf '*\n!.gitignore\n' > "$dir/.gitignore"
    done
    # Expected: each existing storage subdir ends up with its "* / !.gitignore" file
    • ✅ Every existing storage/** directory carries a .gitignore.

Stage the marker, the installer-touched ignore files, lock files, and built assets, then push.

  1. Stage the install artifacts.

    Terminal window
    git add -A storage/
    git add composer.lock package-lock.json public/build/ 2>/dev/null
    # Expected: marker, ignore files, lock files, and build output staged
    • ✅ All install artifacts are staged.
  2. Commit and push — single bundle for a first run, split by concern for a mixed rerun.

    Terminal window
    git commit -m "Phase 3: Local dev environment + installer complete"
    git push origin "$(git rev-parse --abbrev-ref HEAD)"
    git status # working tree clean
    # Expected: commit created, branch pushed, "nothing to commit, working tree clean"
    • ✅ The commit is pushed and the working tree is clean.

For a first run, a single commit reads cleanly. For a rerun that mixes a previous session’s staged work with today’s changes, split by concern:

StrategyWhen
Single bundleFirst run, or small rerun of the same concern
Split by concernPrevious session’s work is topically different (e.g. Boost install vs schema rebuild)

CodeCanyon authors frequently patch packages inside vendor/. Those patches vanish on composer update — so document them now.

  1. Skip if there’s nothing to compare — log the skip reason if vendor/ was composer-installed from the lock file.

    Terminal window
    find _Source -maxdepth 4 -type d -name vendor 2>/dev/null | head -1 || echo "no shipped vendor — skip"
    # Expected: a shipped-vendor path, or "no shipped vendor — skip"
    • ✅ Whether a shipped vendor/ exists to diff is established.
  2. Install a fresh copy in a temp dir and diff against the shipped tree.

    Terminal window
    TEMP_DIR=$(mktemp -d)
    cp composer.json composer.lock "$TEMP_DIR/"
    (cd "$TEMP_DIR" && composer install --no-scripts --no-autoloader 2>&1 | tail -5)
    diff -rq vendor/ "$TEMP_DIR/vendor/" 2>/dev/null | grep -v "^Only in $TEMP_DIR" | head -30
    [ -n "$TEMP_DIR" ] && [ -d "$TEMP_DIR" ] && rm -rf "$TEMP_DIR"
    # Expected: a list of differing/added files (author patches), or no output if clean
    • ✅ Author patches and additions are surfaced.
  3. Record both in _CUSTOMIZATIONS.md at the project root — package, file, what changed, and a reminder to re-apply on update. This is the file the deploy pipeline reads before ever running composer update.

    • _CUSTOMIZATIONS.md documents every vendor patch (or the documented skip reason).

3b. Investigate (don’t delete) files tracked under storage/

Section titled “3b. Investigate (don’t delete) files tracked under storage/”

Laravel’s storage/ holds runtime data, and Deployer swaps the release’s storage/ for a symlink on every deploy — so files committed under storage/ never reach the running app via the release copy. But deleting them blindly is dangerous: many vendor packages read runtime config from storage_path('<file>'), and git rm-ing such a file can break the package silently on the next run. The goal here is to investigate, not delete.

  1. List the suspect files — everything tracked under storage/ that isn’t a standard marker.

    Terminal window
    ALL=$(git ls-files storage/ 2>/dev/null)
    SUSPECT=$(echo "$ALL" | grep -vE '\.gitkeep$|\.gitignore$|^storage/installed$')
    [ -z "$SUSPECT" ] && echo "✅ Only standard markers tracked — nothing to investigate" \
    || { echo "Investigate:"; echo "$SUSPECT" | sed 's/^/ ⚠️ /'; }
    # Expected: either the all-clear, or a short list of files to scan
    • ✅ The suspect list (if any) is known.
  2. For each suspect, scan for a storage_path() reference across vendor, in-tree packages, and project code.

    Terminal window
    FILE="storage/<SUSPECT_FILE>"; BASENAME=$(basename "$FILE")
    grep -rn "$BASENAME\|storage_path.*$BASENAME" vendor/ packages/ app/ config/ routes/ resources/ 2>/dev/null | head -10
    wc -c "$FILE" 2>/dev/null; head -3 "$FILE" 2>/dev/null
    # Expected: any line that reads storage_path('<basename>') means the file is LIVE
    • ✅ Each suspect is classified by where it is read.
Section titled “4. Verify deployment symlinks & shared files”

Page 3 created public/storage (and any addon symlinks) locally. Deployer must recreate them on every release, and any shared_files entry that doesn’t exist on disk breaks the first deploy.

  1. Confirm the local symlinks exist.

    Terminal window
    ls -la public/ | grep "^l" # expect storage (+ packages/addons if used)
    # Expected: at least the storage symlink, plus any addon symlinks from page 3
    • public/storage (and any addon symlinks) are present.
  2. Verify every deploy.php shared_files entry exists on disk.

    Terminal window
    php -r '
    $src = file_get_contents("deploy.php");
    $c = ["shared_files" => []];
    foreach (["set", "add"] as $fn) {
    if (preg_match_all("/\\b".$fn."\\s*\\(\\s*[\'\"]shared_files[\'\"]\\s*,\\s*\\[(.*?)\\]\\s*\\)/s", $src, $ms, PREG_SET_ORDER)) {
    foreach ($ms as $m) {
    preg_match_all("/[\'\"]([^\'\"]+)[\'\"]/", $m[1], $p);
    $c["shared_files"] = array_merge($c["shared_files"], $p[1]);
    }
    }
    }
    $c["shared_files"] = array_values(array_unique($c["shared_files"]));
    if (!$c["shared_files"]) { fwrite(STDERR, "❌ ABORT: no shared_files paths parsed from deploy.php\n"); exit(1); }
    foreach (($c["shared_files"] ?? []) as $f) echo $f, "\n";
    ' | while IFS= read -r f; do
    [ -f "$f" ] && echo "$f" || echo "$f — MISSING; first deploy will fail"
    done
    # Expected: at least one path parsed; a ✅ per shared_files entry; fix any ❌ before deploying
    • ✅ Every shared_files entry resolves to a real file.
  3. Prove the storage symlink serves a real file end-to-end.

    Terminal window
    TEST="storage/app/public/test-$(date +%s).txt"; echo "ok-$(date +%s)" > "$TEST"
    URL="https://[PROJECT_NAME].test/storage/$(basename "$TEST")"
    [ "$(curl -s "$URL")" = "$(cat "$TEST")" ] && echo "✅ symlink + nginx wired" || echo "❌ chain broken"
    rm -f "$TEST"
    # Expected: "✅ symlink + nginx wired" (HTTP 200 with matching content)
    • ✅ The served content matches the file — symlink + nginx chain is wired.

Left open, these routes let anyone reset the app or alter its schema. Use layered defense — two independent layers that block before PHP boots.

  1. Layer 1 — .htaccess. Add these right after RewriteEngine On in public/.htaccess, before the Laravel rewrite block.

    # Block installer + updater routes at Apache level — PHP never loads.
    RewriteRule ^install(/.*)?$ - [F,L]
    RewriteRule ^update(/.*)?$ - [F,L]
    RewriteRule ^upgrade-script$ - [F,L]
    RewriteRule ^update-manual(/.*)?$ - [F,L]
    # If your app has a separate /updater package, uncomment:
    # RewriteRule ^updater(/.*)?$ - [F,L]
    • ✅ The installer/updater rewrite rules sit before the Laravel block.
  2. Commit the .htaccess change.

    Terminal window
    git add public/.htaccess && git commit -m "Block /install and /update routes at Apache level"
    # Expected: the .htaccess rule commit is created
    • public/.htaccess is committed.

^install(/.*)?$ matches /install, /install/, and /install/anything — but not /install-extension (a legitimate admin route in some apps). [F] returns 403 instantly.

Now add the edge layer — a Cloudflare WAF rule that blocks the same paths for all public internet traffic.

  1. Layer 2 — Cloudflare WAF. Check the rule count first (free plan allows 5 custom rules), then add a rule expression that blocks the same path prefixes (http.request.uri.path contains "/install", /update, …) with a block action.

    • ✅ A Cloudflare WAF rule blocks the installer/updater paths at the edge.
  2. Verify both layers from the terminal.

    Terminal window
    curl -sI -o /dev/null -w "install: HTTP %{http_code}\n" https://[PROJECT_NAME].test/install
    curl -sI -o /dev/null -w "update: HTTP %{http_code}\n" https://[PROJECT_NAME].test/update
    # Expected: 403 (Apache) or blocked at the edge — a 200 means a layer isn't active
    • ✅ Both routes return 403 / are blocked.

To temporarily allow access for a legitimate update: comment out the .htaccess rules via SSH (and pause the WAF rule), run the update, then restore both.

Do not mark this step done until every box below is checked.

  • 🤖 Marker trackedstorage/installed tracked; all storage/**/.gitignore files present.
  • 🤖 Committed + pushed — Phase-3 changes committed and pushed; working tree clean.
  • 🤖 Patches documented_CUSTOMIZATIONS.md records vendor patches (or the documented skip reason).
  • 🤖 Deploy paths verified — every deploy.php shared_files entry exists; storage symlink passes the HTTP content test.
  • 🔀 Routes locked/install and /update return 403 / are blocked at both layers (👤 WAF + 🤖 .htaccess).