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 Compose files 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 secrets in 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 files and 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/secrets and 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 YAML as 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 -eu fails 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 exec hands 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_PASS and API_KEY above appear as files (/run/secrets/DB_PASS, /run/secrets/API_KEY); the mount­ed with-secrets.sh wrap­per script exports them as DB_PASS and API_KEY envi­ron­ment vari­ables for your-original-command --with-args.

    Decision points and alternatives

    • Prefer native *_FILE sup­port if 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 secrets are 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: Never print 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 git or oth­er ver­sion con­trol
      Mitigation: Keep secret files out of repos, add strict .gitignore rules, 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 Code is a pow­er­ful command-​line tool for agen­tic soft­ware devel­op­ment. However, if you try to use it over an SSH secure shell ses­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 OAuth token 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 claude over 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_UNLOCKED to guard against mul­ti­ple attempts
    3. Delegate to the real claude com­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 Pro login 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-keychain com­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 perlcritic tool 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­enced in the past.)

    You can use perlcritic at the com­mand line, inte­grat­ed with your edi­tor, as a git pre-​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.

    perlcritic enables the fol­low­ing poli­cies by default unless you cus­tomize its con­fig­u­ra­tion or install more. These are just the ​“gen­tle” (sever­i­ty lev­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­cies goes 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 themes so 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 perlcritic is 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 have good rea­sons to dis­agree before you either con­fig­ure perlcritic appro­pri­ate­ly or selec­tive­ly and know­ing­ly bend the rules where 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­cies group 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 perlcritic on the ​“bru­tal” set­ting before com­mit­ting my own. What do you pre­fer? Let me know in the com­ments below.