惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
WordPress大学
WordPress大学
Y
Y Combinator Blog
I
InfoQ
美团技术团队
罗磊的独立博客
B
Blog RSS Feed
GbyAI
GbyAI
小众软件
小众软件
IT之家
IT之家
Engineering at Meta
Engineering at Meta
Blog — PlanetScale
Blog — PlanetScale
V
V2EX
Last Week in AI
Last Week in AI
酷 壳 – CoolShell
酷 壳 – CoolShell
Jina AI
Jina AI
MyScale Blog
MyScale Blog
博客园 - 聂微东
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
The GitHub Blog
The GitHub Blog
T
The Blog of Author Tim Ferriss

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
Why an encrypted config backup breaks when you move serve...
Nasrul Hazim Bin Mohamad · 2026-06-12 · via DEV Community

Imagine you write a letter in a secret code that only your old house key can read. Then you move. You photocopy the coded letter, carry it to the new house… and realise the new key can't decode any of it. The letter is valid, just useless.

That's effectively what happens when you back up encrypted values from a Laravel database and restore them onto a different server. I hit exactly this while working on laravel-config-backup today, so here's the problem and the fix.

The real cause: Crypt is bound to APP_KEY

When you store sensitive settings (think API tokens or OAuth secrets) in the database, you typically encrypt them with Crypt::encryptString(). Lovely — until you remember Crypt uses your app's APP_KEY as the key.

A naive backup copies that ciphertext straight across:

// Naive approach — move the ciphertext as-is
$value = DB::table('settings')->where('key', 'some.secret')->value('value');
// this value is encrypted with the OLD server's APP_KEY

The new server has a different APP_KEY. Try to decrypt → DecryptException: The payload is invalid. Your backup is technically complete but practically dead.

The fix: decrypt on the way out, re-encrypt on the way in

The decision is easy to state, hard to stay disciplined about: never carry ciphertext across a server boundary. Instead —

  1. On create: decrypt the values with the source server's APP_KEY, store plaintext inside the archive.
  2. Protect that archive with AES-256 and a password (a human-held secret, not the APP_KEY).
  3. On restore: re-encrypt the values with the destination server's APP_KEY before writing to the DB.

Back to the analogy: you decode the letter, carry the plain letter in a locked briefcase (the password-protected archive), and re-encode it with the new house's lock on arrival. The briefcase handles security in transit — not the old code that's no longer relevant.

I made that intent explicit right where the behaviour lives, in ConfigBackupService:

/**
 * Config Backup & Restore.
 *
 * Bundles .env + DB-stored settings into a single AES-256, password-encrypted
 * ZIP. Content inside the archive is stored DECRYPTED so the encrypted DB
 * columns are re-encrypted on import with the destination server's APP_KEY —
 * making a backup portable across servers.
 */
class ConfigBackupService { /* ... */ }

"Naked" plaintext inside the archive sounds scary, but the security boundary has moved on purpose: from the APP_KEY (which you want to differ per server) to the archive password (which you control and can rotate). That's the right trade-off for an artifact whose whole job is to move.

Authz: one source of truth, not scattered checks

The same pass hardened authorization. It's too easy to scatter gate checks across the UI, routes, and commands. One method everything refers to keeps it honest:

/**
 * Whether the current context passes the configured authorization gate.
 * Returns true when no gate is configured. CLI commands run by a server
 * operator deliberately bypass this. Single source of truth for authz.
 */
public function authorizes(): bool
{
    $gate = $this->gate();

    return $gate === null || Gate::allows($gate);
}

Two subtle but important points:

  • The gate is nullable. If the host app doesn't set a gate, the package doesn't impose its own policy — you can rely on route middleware. Good tooling suggests, it doesn't dictate.
  • The CLI deliberately bypasses it. Someone running php artisan config-backup:create on the server already has shell access. Forcing them through a web gate is theatre, not security.

Keep it honest with a test

The portability part is hard to verify "by eye". I keep it honest with a round-trip test: encrypt with one key, simulate a different key, and assert the restore can still read the original value.

it('restores secrets under a different APP_KEY', function () {
    config(['app.key' => 'base64:'.base64_encode(random_bytes(32))]);
    DB::table('settings')->insert([
        'key' => 'some.secret',
        'value' => Crypt::encryptString('super-secret'),
    ]);

    $backup = app(ConfigBackupService::class)->create(password: 'pa55');

    // Simulate the destination server: a different APP_KEY
    config(['app.key' => 'base64:'.base64_encode(random_bytes(32))]);
    DB::table('settings')->truncate();

    app(ConfigBackupService::class)->restore($backup, password: 'pa55');

    $value = DB::table('settings')->where('key', 'some.secret')->value('value');
    expect(Crypt::decryptString($value))->toBe('super-secret');
});

If this stays green across two different APP_KEYs, you know your backup is genuinely portable — not just "works on my machine".

The takeaway

Whenever you design something that crosses a boundary — server, environment, tenant — ask: which key is glued to this artifact, and does that key exist on the other side? The answer is often no, and the safest thing to carry is plaintext in a container whose key you control — not ciphertext bound to a key you left behind.

The package is open source: github.com/cleaniquecoders/laravel-config-backup.