Tag: Shell

  • 10 Lines to Better Docker Compose Secrets

    10 Lines to Better Docker Compose Secrets

    This is a prac­ti­cal pat­tern I use when con­tainer­ized apps expect envi­ron­ment vari­ables but I want the secu­ri­ty ben­e­fits of file-​mounted secrets. Drop the shell script below next to your Docker Composefiles and you can do the same.

    Quick overview

    Secrets like pass­words and API keys belong out­side your repos­i­to­ry and appli­ca­tion image lay­ers. Docker Compose can mount such secretsin your con­tain­ers as files under /run/secrets, which keeps them out of images and ver­sion con­trol. But many apps still expect con­fig­u­ra­tion via envi­ron­ment vari­ables. Rather than chang­ing app code, I use a tiny wrap­per script that:

    • reads every file in /run/secrets
    • exports each file’s con­tents as an envi­ron­ment variable
    • then execs the orig­i­nal command

    It’s small, pre­dictable, portable, and keeps secrets from mix­ing with your ver­sioned .env envi­ron­ment filesand out of your Compose files.

    How it works

    • Location:Docker Compose mounts secrets into the con­tain­er at /run/secrets/<NAME>.
    • Mapping rule:The wrap­per uses those file names as envi­ron­ment vari­able names; the file con­tents become the val­ues. Secret names in your Compose file must be valid shell iden­ti­fiers(they become both the file names in /run/secretsand the export­ed vari­able names).
    • Execution:After export­ing vari­ables, the script uses exec "$@"so that the wrapped process replaces the shell and inher­its the export­ed environment.
    • Security mod­el:Secrets remain files you can per­mis­sion appro­pri­ate­ly on the host; they’re not baked into images or stored in your Compose YAMLas plain text.

    The script

    Let’s call it with-secrets.sh:

    #!/bin/sh
    set -eu
    
    for secret_file in /run/secrets/*; do
      [ -e "$secret_file" ] || continue
      if [ -f "$secret_file" ]; then
        name=$(basename "$secret_file")
        export "$name=$(cat "$secret_file")"
      fi
    done
    
    exec "$@"

    Notes about the script

    • set -eufails fast on unset vari­ables or errors.
    • Since it exports each secret using the file name as the vari­able name, san­i­tize the file name if you need dif­fer­ent envi­ron­ment vari­able names.
    • The final exechands con­trol to your app with­out leav­ing an extra shell process.

    Example Compose snippet

    services:
      app:
        image: your-app:latest
        secrets:
          - DB_PASS
          - API_KEY
        volumes:
          - ./with-secrets.sh:/with-secrets.sh:ro
        command: ["/with-secrets.sh", "your-original-command", "--with-args"]
    
    secrets:
      DB_PASS:
        file: ./secrets/db_password.txt
      API_KEY:
        file: ./secrets/api_key.txt

    Behavior: DB_PASSand API_KEYabove appear as files (/run/secrets/DB_PASS, /run/secrets/API_KEY); the mount­ed with-secrets.shwrap­per script exports them as DB_PASSand API_KEYenvi­ron­ment vari­ables for your-original-command --with-args.

    Decision points and alternatives

    • Prefer native *_FILEsup­portif your app sup­ports it (e.g., PostgreSQL’s PGPASSFILE). That avoids the wrap­per entirely.
    • For multi-​host or high-​compliance deploy­ments,use an exter­nal secrets man­ag­er (e.g., Hashicorp Vault, cloud KMS, SOPS) rather than Compose secrets.
    • Build-​time secretsare a sep­a­rate con­cern; use BuildKit or ded­i­cat­ed build secret mech­a­nisms to avoid leak­ing cre­den­tials into your image layers.

    Risks and mitigations

    • Risk:Accidentally log­ging or dump­ing envi­ron­ment vari­ables
      Mitigation: Neverprint envi­ron­ment vari­ables in logs and restrict debug output
    • Risk:Secret file names that are not valid shell iden­ti­fiers
      Mitigation:Normalize or map file names to safe envi­ron­ment vari­able names before exporting
    • Risk:Secrets checked into gitor oth­er ver­sion con­trol
      Mitigation:Keep secret files out of repos, add strict .gitignorerules, and inject secrets via CI/​CD or run­time provisioning

    Final notes

    This pat­tern is inten­tion­al­ly prag­mat­ic: it pre­serves the secu­ri­ty advan­tage of file-​mounted secrets while let­ting unmod­i­fied apps keep using envi­ron­ment vari­ables. It’s not a sil­ver bul­let for every environment–use it where Compose secrets are appro­pri­ate and pair it with stronger secret stores for production-​grade, multi-​host deployments.

  • Claude Code CLI over SSH on macOS: Fixing Keychain Access

    Claude Code CLI over SSH on macOS: Fixing Keychain Access

    Claude Codeis a pow­er­ful command-​line tool for agen­ticsoft­ware devel­op­ment. However, if you try to use it over an SSH secure shellses­sion on macOS, you may see a con­fus­ing mix of ​“Login suc­cess­ful” and ​“Missing API key” mes­sages. The root cause: Claude Code’s OAuthtoken lives in the macOS Keychain, which SSH ses­sions can’t access by default.

    Here’s a quick fix that took about 10 min­utes to build — with Claude Code’s help. (Meta, but effective.)

    The Fix

    Add this to your ~/.zshrc:

    # Wrapper function to unlock keychain before running claude
    claude() {
      if [ -n "$SSH_CONNECTION" ] && [ -z "$KEYCHAIN_UNLOCKED" ]
      then
        security unlock-keychain ~/Library/Keychains/login.keychain-db
        export KEYCHAIN_UNLOCKED=true
      fi
      command claude "$@"
    }

    Reload your shell (source ~/.zshrc), then run claudeover SSH. It will prompt for your key­chain pass­word once per ses­sion, then work normally.

    How It Works

    1. Detects SSH ses­sions via $SSH_CONNECTION
    2. Unlock the key­chain once per ses­sion, using $KEYCHAIN_UNLOCKEDto guard against mul­ti­ple attempts
    3. Delegate to the real claudecom­mand with all argu­ments passed

    The key­chain stays unlocked for the dura­tion of your SSH ses­sion, so you only enter the pass­word once.

    Security note:This does­n’t bypass macOS Keychain secu­ri­ty. It just prompts you once per SSH ses­sion, the same as if you’d unlocked it locally.

    With this wrap­per in place, I can get Claude Code to behave over SSH exact­ly as it does local­ly. There are no sur­pris­es and no API keys, and my Claude Prologin works as expected.

    The broader lesson

    Command line tools that rely on the macOS Keychain often break over SSH. Wrapping those tools with the security unlock-keychaincom­mand gen­er­al­ly fix­es those issues.

  • Everyone’s a (Perl) critic, and you can be too!

    Everyone’s a (Perl) critic, and you can be too!

    The perlcritictool is often your first defense against ​“awk­ward, hard to read, error-​prone, or uncon­ven­tion­al con­structs in your code,” per its descrip­tion. It’s part of a class of pro­grams his­tor­i­cal­ly known as lin­ters, so-​called because like a clothes dry­er machine’s lint trap, they ​“detect small errors with big effects.” (Another such lin­ter is perltidy, which I’ve ref­er­encedin the past.)

    You can use perlcritic at the com­mand line, inte­grat­ed with your edi­tor, as a gitpre-​commit hook, or (my pref­er­ence) as part of your author tests. It’s dri­ven by poli­cies, indi­vid­ual mod­ules that check your code against a par­tic­u­lar rec­om­men­da­tion, many of them from Damian Conway’s Perl Best Practices(2005). Those poli­cies, in turn, are enabled by PPI, a library that trans­forms Perl code into doc­u­ments that can be pro­gram­mat­i­cal­ly exam­ined and manip­u­lat­ed much like the Document Object Model (DOM)is used to pro­gram­mat­i­cal­ly access web pages.

    perlcriticenables the fol­low­ing poli­cies by default unless you cus­tomize its con­fig­u­ra­tionor install more. These are just the ​“gen­tle” (sever­i­tylev­el 5) poli­cies, so con­sid­er them the bare min­i­mum in detect­ing bad prac­tices. The full set of includ­ed poli­ciesgoes much deep­er, ratch­et­ing up the sever­i­ty to ​“stern,” ​“harsh,” ​“cru­el,” and ​“bru­tal.” They’re fur­ther orga­nized accord­ing to themesso that you might selec­tive­ly review your code against issues like secu­ri­ty, main­te­nance, com­plex­i­ty, and bug prevention.

    My favorite above is prob­a­bly ProhibitEvilModules. Aside from the col­or­ful name, a devel­op­ment team can use it to steer peo­ple towards an organization’s favored solu­tions rather than ​“dep­re­cat­ed, bug­gy, unsup­port­ed, or inse­cure” ones. By default, it pro­hibits Class::ISA, Pod::Plainer, Shell, and Switch, but you should curate and con­fig­ure a list with­in your team.

    Speaking of work­ing with­in a team, although perlcriticis meant to be a vital tool to ensure good prac­tices, it’s no sub­sti­tute for man­u­al peer code review. Those reviews can lead to the cre­ation or adop­tion of new auto­mat­ed poli­cies to save time and set­tle argu­ments, but such work should be done col­lab­o­ra­tive­ly after achiev­ing some kind of con­sen­sus. This is true whether you’re a team of employ­ees work­ing on pro­pri­etary soft­ware or a group of vol­un­teers devel­op­ing open source.

    Of course, rea­son­able peo­ple can and do dis­agree over any of the includ­ed poli­cies, but as a rea­son­able per­son, you should havegoodrea­sons to dis­agree before you either con­fig­ure perlcriticappro­pri­ate­ly or selec­tive­ly and know­ing­ly bend the ruleswhere required. Other CPAN authors have even pro­vid­ed their own addi­tions to perlcritic, so it’s worth search­ing CPAN under ​“Perl::Critic::Policy::”for more exam­ples. In par­tic­u­lar, these community-​inspired poli­ciesgroup a num­ber of rec­om­men­da­tions from Perl devel­op­ers on Internet Relay Chat (IRC).

    Personally, although I adhere to my employer’s stan­dard­ized con­fig­u­ra­tion when test­ing and review­ing code, I like to run perlcriticon the ​“bru­tal” set­ting before com­mit­ting my own. What do you pre­fer? Let me know in the com­ments below.