Skip to content

One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

Description

@ineedjet

Context

Right now a target has one (or more, via env_refs) vault(s) whose combined
env: covers every app running on that target, merged into one shared
.env. Downstream, generate-env.sh/lib.sh's generate_env() filters
that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
{APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
kept from seeing each other's variables today. The prefix convention exists
specifically because everything gets poured into one shared canvas first
and has to be sorted back out afterward, and it leaks into the compose
files themselves: apps/traefik/docker-compose.yml references
${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
compose files already aren't vanilla-compose-compatible because of this.

An earlier version of this issue proposed fixing that by naming vaults
after the app they belong to (vaults/traefik.yml) and relying on the
filename matching the app name as the link. That's wrong: it reintroduces
exactly the kind of implicit, convention-based wiring this repo has
consistently avoided elsewhere (explicit app_refs/env_refs, "matching
filenames are a convenience, not an implicit relationship" for
vault/target pairing). It also doesn't resolve on its own — see below.

Proposal

Make the target the explicit wiring point between an app and the vault(s)
that feed it, the same way it already explicitly wires app_refs and
env_refs today — just scoped per app instead of per target:

apps:
traefik:
env_refs:
- rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
env_refs:
- rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

replacing the current flat apps: [traefik, rybbit] list — the app names
are just the mapping's keys now, so nothing needed for deploy.sh's
APPS= synthesis is lost.

Each app's vault can then declare its env output names directly, with zero
prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

asset: hawkeye-traefik.sops.envkeys:
- hawkeyeenv:
HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

HTTP_PORT is exactly what docker-compose.yml can reference — no
prefix stripped anywhere downstream, no hidden rename. Since the vault is
referenced explicitly by ref (not discovered by filename), its filename
doesn't need to match the app name at all — asset names just need to be
unique within a release, same as any other bundle asset.

This resolves what the filename-convention version of this issue couldn't:

  • No asset-naming collision risk across targets. Nothing depends on
    the vault's filename matching the app's name, so two different targets
    running the same app never fight over an asset name.
  • Collisions are detected exactly where they're real, not where they
    aren't.
    Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
    app's own env_refs list. Across different apps there was never a
    real merge to begin with — a value like DOMAIN duplicated as the same
    mapping in two apps' vaults is structurally guaranteed to resolve to the
    same GitHub Secret value; the only way it drifts is a typo in one
    mapping, which is an ordinary authoring mistake, not a lost safety net.
  • Cross-repo secret composition falls out for free, no new mechanism
    needed.
    This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
    CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
    that needs to contribute e.g. a Telegram bot token just needs a copy of
    the target's public key (keys/hawkeye.pub — not secret, freely
    copyable to any repo, any org) and to run the already-published
    encrypt-env action itself in its own release workflow. It publishes
    its own encrypted asset to its own release. The target then simply adds
    that ref to whichever app's env_refs needs it:
    owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
    env_refs loop already parses the repo out of the ref itself (not
    hardcoded to flightdeck's own repo) and decrypts with the target's own
    SOPS_AGE_KEY_FILE regardless of which repo published the asset —
    decryption is keyed by recipient, not by source repo. Zero code
    changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
    superseded by this.

Resolved: how per-app env actually reaches the server

Settled during discussion with #111 (Ansible→Fabric), then superseded by
a bigger decision made in the same implementation session: flightdeck has
no manual administration flow at all, ever — the "manual local
quick-start" path described below was deleted outright, not kept as a
separate untouched path.

  • Automated path is now the only path. Ref-resolution and downloading
    run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
    runner pushes the finished result to the server rather than the server
    pulling anything.
  • Decryption and config-template rendering both happen on the runner
    too
    (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
    planned here. The server never runs sops, never holds an age key,
    never runs envsubst. It receives a real, already-decrypted .env and
    already-rendered config directly.
  • Collision detection still happens in CI, from ciphertext, before
    anything is decrypted or pushed
    — this part of the design didn't
    change. SOPS's dotenv output only encrypts values — key names stay in
    cleartext in the downloaded .sops.env asset itself
    (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
    entry, deploy/collisions.py compares key names across the downloaded
    encrypted files directly, no decryption needed. Any duplicate key fails
    the build before anything is decrypted.
  • Net result: no shell scripting left on the server at all.
    up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
    are all deleted, no replacements. The server's only dependencies are
    Docker and Docker Compose.

Related

Builds on #104/#108/#110 (env_refs as a list, apps living on the
target). Supersedes and closes #113 (cross-vault import) — same goal,
solved by this design without new machinery. Implemented together with
#111 (Ansible→Fabric) — this is the concrete feature the push-based
model in #111 was designed to carry, not built separately in Ansible.
Also affects #103 (ansible ref-resolution dedup), which becomes moot
under #111 rather than needing its own fix.

Implemented, together with #111 and #116, going further than originally
scoped here (see the updated section above).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      , 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
       blocks
      (function() {
      function addCopyButtons() {
      document.querySelectorAll('pre code').forEach(function(codeBlock) {
      if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
      codeBlock.parentElement.setAttribute('data-copy-added', 'true');
      var btn = document.createElement('button');
      btn.textContent = 'Copy';
      btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
      btn.onmouseover = function() { this.style.opacity = '1'; };
      btn.onmouseout = function() { this.style.opacity = '0.7'; };
      btn.onclick = function() {
      navigator.clipboard.writeText(codeBlock.textContent).then(function() {
      btn.textContent = 'Copied!';
      setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
      });
      };
      codeBlock.parentElement.style.position = 'relative';
      codeBlock.parentElement.appendChild(btn);
      });
      }
      addCopyButtons();
      // Re-run on dynamic content
      var observer = new MutationObserver(addCopyButtons);
      observer.observe(document.body, { childList: true, subtree: true });
      })();
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
      Skip to content

      One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

      Description

      @ineedjet

      Context

      Right now a target has one (or more, via env_refs) vault(s) whose combined
      env: covers every app running on that target, merged into one shared
      .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
      that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
      {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
      kept from seeing each other's variables today. The prefix convention exists
      specifically because everything gets poured into one shared canvas first
      and has to be sorted back out afterward, and it leaks into the compose
      files themselves: apps/traefik/docker-compose.yml references
      ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
      compose files already aren't vanilla-compose-compatible because of this.

      An earlier version of this issue proposed fixing that by naming vaults
      after the app they belong to (vaults/traefik.yml) and relying on the
      filename matching the app name as the link. That's wrong: it reintroduces
      exactly the kind of implicit, convention-based wiring this repo has
      consistently avoided elsewhere (explicit app_refs/env_refs, "matching
      filenames are a convenience, not an implicit relationship" for
      vault/target pairing). It also doesn't resolve on its own — see below.

      Proposal

      Make the target the explicit wiring point between an app and the vault(s)
      that feed it, the same way it already explicitly wires app_refs and
      env_refs today — just scoped per app instead of per target:

      apps:
      traefik:
      env_refs:
      - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
      env_refs:
      - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

      replacing the current flat apps: [traefik, rybbit] list — the app names
      are just the mapping's keys now, so nothing needed for deploy.sh's
      APPS= synthesis is lost.

      Each app's vault can then declare its env output names directly, with zero
      prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

      asset: hawkeye-traefik.sops.envkeys:
      - hawkeyeenv:
      HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

      HTTP_PORT is exactly what docker-compose.yml can reference — no
      prefix stripped anywhere downstream, no hidden rename. Since the vault is
      referenced explicitly by ref (not discovered by filename), its filename
      doesn't need to match the app name at all — asset names just need to be
      unique within a release, same as any other bundle asset.

      This resolves what the filename-convention version of this issue couldn't:

      • No asset-naming collision risk across targets. Nothing depends on
        the vault's filename matching the app's name, so two different targets
        running the same app never fight over an asset name.
      • Collisions are detected exactly where they're real, not where they
        aren't.
        Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
        app's own env_refs list. Across different apps there was never a
        real merge to begin with — a value like DOMAIN duplicated as the same
        mapping in two apps' vaults is structurally guaranteed to resolve to the
        same GitHub Secret value; the only way it drifts is a typo in one
        mapping, which is an ordinary authoring mistake, not a lost safety net.
      • Cross-repo secret composition falls out for free, no new mechanism
        needed.
        This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
        CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
        that needs to contribute e.g. a Telegram bot token just needs a copy of
        the target's public key (keys/hawkeye.pub — not secret, freely
        copyable to any repo, any org) and to run the already-published
        encrypt-env action itself in its own release workflow. It publishes
        its own encrypted asset to its own release. The target then simply adds
        that ref to whichever app's env_refs needs it:
        owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
        env_refs loop already parses the repo out of the ref itself (not
        hardcoded to flightdeck's own repo) and decrypts with the target's own
        SOPS_AGE_KEY_FILE regardless of which repo published the asset —
        decryption is keyed by recipient, not by source repo. Zero code
        changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
        superseded by this.

      Resolved: how per-app env actually reaches the server

      Settled during discussion with #111 (Ansible→Fabric), then superseded by
      a bigger decision made in the same implementation session: flightdeck has
      no manual administration flow at all, ever — the "manual local
      quick-start" path described below was deleted outright, not kept as a
      separate untouched path.

      • Automated path is now the only path. Ref-resolution and downloading
        run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
        runner pushes the finished result to the server rather than the server
        pulling anything.
      • Decryption and config-template rendering both happen on the runner
        too
        (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
        planned here. The server never runs sops, never holds an age key,
        never runs envsubst. It receives a real, already-decrypted .env and
        already-rendered config directly.
      • Collision detection still happens in CI, from ciphertext, before
        anything is decrypted or pushed
        — this part of the design didn't
        change. SOPS's dotenv output only encrypts values — key names stay in
        cleartext in the downloaded .sops.env asset itself
        (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
        entry, deploy/collisions.py compares key names across the downloaded
        encrypted files directly, no decryption needed. Any duplicate key fails
        the build before anything is decrypted.
      • Net result: no shell scripting left on the server at all.
        up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
        are all deleted, no replacements. The server's only dependencies are
        Docker and Docker Compose.

      Related

      Builds on #104/#108/#110 (env_refs as a list, apps living on the
      target). Supersedes and closes #113 (cross-vault import) — same goal,
      solved by this design without new machinery. Implemented together with
      #111 (Ansible→Fabric) — this is the concrete feature the push-based
      model in #111 was designed to carry, not built separately in Ansible.
      Also affects #103 (ansible ref-resolution dedup), which becomes moot
      under #111 rather than needing its own fix.

      Implemented, together with #111 and #116, going further than originally
      scoped here (see the updated section above).

      Metadata

      Metadata

      Assignees

      No one assigned

        Labels

        No labels
        No labels

        Type

        No type

        Projects

        No projects

          Milestone

          No milestone

          Relationships

          None yet

          Development

          No branches or pull requests

          Issue actions

          , 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
          Skip to content

          One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

          Description

          @ineedjet

          Context

          Right now a target has one (or more, via env_refs) vault(s) whose combined
          env: covers every app running on that target, merged into one shared
          .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
          that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
          {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
          kept from seeing each other's variables today. The prefix convention exists
          specifically because everything gets poured into one shared canvas first
          and has to be sorted back out afterward, and it leaks into the compose
          files themselves: apps/traefik/docker-compose.yml references
          ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
          compose files already aren't vanilla-compose-compatible because of this.

          An earlier version of this issue proposed fixing that by naming vaults
          after the app they belong to (vaults/traefik.yml) and relying on the
          filename matching the app name as the link. That's wrong: it reintroduces
          exactly the kind of implicit, convention-based wiring this repo has
          consistently avoided elsewhere (explicit app_refs/env_refs, "matching
          filenames are a convenience, not an implicit relationship" for
          vault/target pairing). It also doesn't resolve on its own — see below.

          Proposal

          Make the target the explicit wiring point between an app and the vault(s)
          that feed it, the same way it already explicitly wires app_refs and
          env_refs today — just scoped per app instead of per target:

          apps:
          traefik:
          env_refs:
          - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
          env_refs:
          - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

          replacing the current flat apps: [traefik, rybbit] list — the app names
          are just the mapping's keys now, so nothing needed for deploy.sh's
          APPS= synthesis is lost.

          Each app's vault can then declare its env output names directly, with zero
          prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

          asset: hawkeye-traefik.sops.envkeys:
          - hawkeyeenv:
          HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

          HTTP_PORT is exactly what docker-compose.yml can reference — no
          prefix stripped anywhere downstream, no hidden rename. Since the vault is
          referenced explicitly by ref (not discovered by filename), its filename
          doesn't need to match the app name at all — asset names just need to be
          unique within a release, same as any other bundle asset.

          This resolves what the filename-convention version of this issue couldn't:

          • No asset-naming collision risk across targets. Nothing depends on
            the vault's filename matching the app's name, so two different targets
            running the same app never fight over an asset name.
          • Collisions are detected exactly where they're real, not where they
            aren't.
            Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
            app's own env_refs list. Across different apps there was never a
            real merge to begin with — a value like DOMAIN duplicated as the same
            mapping in two apps' vaults is structurally guaranteed to resolve to the
            same GitHub Secret value; the only way it drifts is a typo in one
            mapping, which is an ordinary authoring mistake, not a lost safety net.
          • Cross-repo secret composition falls out for free, no new mechanism
            needed.
            This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
            CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
            that needs to contribute e.g. a Telegram bot token just needs a copy of
            the target's public key (keys/hawkeye.pub — not secret, freely
            copyable to any repo, any org) and to run the already-published
            encrypt-env action itself in its own release workflow. It publishes
            its own encrypted asset to its own release. The target then simply adds
            that ref to whichever app's env_refs needs it:
            owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
            env_refs loop already parses the repo out of the ref itself (not
            hardcoded to flightdeck's own repo) and decrypts with the target's own
            SOPS_AGE_KEY_FILE regardless of which repo published the asset —
            decryption is keyed by recipient, not by source repo. Zero code
            changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
            superseded by this.

          Resolved: how per-app env actually reaches the server

          Settled during discussion with #111 (Ansible→Fabric), then superseded by
          a bigger decision made in the same implementation session: flightdeck has
          no manual administration flow at all, ever — the "manual local
          quick-start" path described below was deleted outright, not kept as a
          separate untouched path.

          • Automated path is now the only path. Ref-resolution and downloading
            run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
            runner pushes the finished result to the server rather than the server
            pulling anything.
          • Decryption and config-template rendering both happen on the runner
            too
            (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
            planned here. The server never runs sops, never holds an age key,
            never runs envsubst. It receives a real, already-decrypted .env and
            already-rendered config directly.
          • Collision detection still happens in CI, from ciphertext, before
            anything is decrypted or pushed
            — this part of the design didn't
            change. SOPS's dotenv output only encrypts values — key names stay in
            cleartext in the downloaded .sops.env asset itself
            (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
            entry, deploy/collisions.py compares key names across the downloaded
            encrypted files directly, no decryption needed. Any duplicate key fails
            the build before anything is decrypted.
          • Net result: no shell scripting left on the server at all.
            up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
            are all deleted, no replacements. The server's only dependencies are
            Docker and Docker Compose.

          Related

          Builds on #104/#108/#110 (env_refs as a list, apps living on the
          target). Supersedes and closes #113 (cross-vault import) — same goal,
          solved by this design without new machinery. Implemented together with
          #111 (Ansible→Fabric) — this is the concrete feature the push-based
          model in #111 was designed to carry, not built separately in Ansible.
          Also affects #103 (ansible ref-resolution dedup), which becomes moot
          under #111 rather than needing its own fix.

          Implemented, together with #111 and #116, going further than originally
          scoped here (see the updated section above).

          Metadata

          Metadata

          Assignees

          No one assigned

            Labels

            No labels
            No labels

            Type

            No type

            Projects

            No projects

              Milestone

              No milestone

              Relationships

              None yet

              Development

              No branches or pull requests

              Issue actions

              , 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
              Skip to content

              One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

              Description

              @ineedjet

              Context

              Right now a target has one (or more, via env_refs) vault(s) whose combined
              env: covers every app running on that target, merged into one shared
              .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
              that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
              {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
              kept from seeing each other's variables today. The prefix convention exists
              specifically because everything gets poured into one shared canvas first
              and has to be sorted back out afterward, and it leaks into the compose
              files themselves: apps/traefik/docker-compose.yml references
              ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
              compose files already aren't vanilla-compose-compatible because of this.

              An earlier version of this issue proposed fixing that by naming vaults
              after the app they belong to (vaults/traefik.yml) and relying on the
              filename matching the app name as the link. That's wrong: it reintroduces
              exactly the kind of implicit, convention-based wiring this repo has
              consistently avoided elsewhere (explicit app_refs/env_refs, "matching
              filenames are a convenience, not an implicit relationship" for
              vault/target pairing). It also doesn't resolve on its own — see below.

              Proposal

              Make the target the explicit wiring point between an app and the vault(s)
              that feed it, the same way it already explicitly wires app_refs and
              env_refs today — just scoped per app instead of per target:

              apps:
              traefik:
              env_refs:
              - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
              env_refs:
              - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

              replacing the current flat apps: [traefik, rybbit] list — the app names
              are just the mapping's keys now, so nothing needed for deploy.sh's
              APPS= synthesis is lost.

              Each app's vault can then declare its env output names directly, with zero
              prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

              asset: hawkeye-traefik.sops.envkeys:
              - hawkeyeenv:
              HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

              HTTP_PORT is exactly what docker-compose.yml can reference — no
              prefix stripped anywhere downstream, no hidden rename. Since the vault is
              referenced explicitly by ref (not discovered by filename), its filename
              doesn't need to match the app name at all — asset names just need to be
              unique within a release, same as any other bundle asset.

              This resolves what the filename-convention version of this issue couldn't:

              • No asset-naming collision risk across targets. Nothing depends on
                the vault's filename matching the app's name, so two different targets
                running the same app never fight over an asset name.
              • Collisions are detected exactly where they're real, not where they
                aren't.
                Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
                app's own env_refs list. Across different apps there was never a
                real merge to begin with — a value like DOMAIN duplicated as the same
                mapping in two apps' vaults is structurally guaranteed to resolve to the
                same GitHub Secret value; the only way it drifts is a typo in one
                mapping, which is an ordinary authoring mistake, not a lost safety net.
              • Cross-repo secret composition falls out for free, no new mechanism
                needed.
                This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
                CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
                that needs to contribute e.g. a Telegram bot token just needs a copy of
                the target's public key (keys/hawkeye.pub — not secret, freely
                copyable to any repo, any org) and to run the already-published
                encrypt-env action itself in its own release workflow. It publishes
                its own encrypted asset to its own release. The target then simply adds
                that ref to whichever app's env_refs needs it:
                owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
                env_refs loop already parses the repo out of the ref itself (not
                hardcoded to flightdeck's own repo) and decrypts with the target's own
                SOPS_AGE_KEY_FILE regardless of which repo published the asset —
                decryption is keyed by recipient, not by source repo. Zero code
                changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
                superseded by this.

              Resolved: how per-app env actually reaches the server

              Settled during discussion with #111 (Ansible→Fabric), then superseded by
              a bigger decision made in the same implementation session: flightdeck has
              no manual administration flow at all, ever — the "manual local
              quick-start" path described below was deleted outright, not kept as a
              separate untouched path.

              • Automated path is now the only path. Ref-resolution and downloading
                run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
                runner pushes the finished result to the server rather than the server
                pulling anything.
              • Decryption and config-template rendering both happen on the runner
                too
                (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
                planned here. The server never runs sops, never holds an age key,
                never runs envsubst. It receives a real, already-decrypted .env and
                already-rendered config directly.
              • Collision detection still happens in CI, from ciphertext, before
                anything is decrypted or pushed
                — this part of the design didn't
                change. SOPS's dotenv output only encrypts values — key names stay in
                cleartext in the downloaded .sops.env asset itself
                (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
                entry, deploy/collisions.py compares key names across the downloaded
                encrypted files directly, no decryption needed. Any duplicate key fails
                the build before anything is decrypted.
              • Net result: no shell scripting left on the server at all.
                up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                are all deleted, no replacements. The server's only dependencies are
                Docker and Docker Compose.

              Related

              Builds on #104/#108/#110 (env_refs as a list, apps living on the
              target). Supersedes and closes #113 (cross-vault import) — same goal,
              solved by this design without new machinery. Implemented together with
              #111 (Ansible→Fabric) — this is the concrete feature the push-based
              model in #111 was designed to carry, not built separately in Ansible.
              Also affects #103 (ansible ref-resolution dedup), which becomes moot
              under #111 rather than needing its own fix.

              Implemented, together with #111 and #116, going further than originally
              scoped here (see the updated section above).

              Metadata

              Metadata

              Assignees

              No one assigned

                Labels

                No labels
                No labels

                Type

                No type

                Projects

                No projects

                  Milestone

                  No milestone

                  Relationships

                  None yet

                  Development

                  No branches or pull requests

                  Issue actions

                  , 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
                  Skip to content

                  One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

                  Description

                  @ineedjet

                  Context

                  Right now a target has one (or more, via env_refs) vault(s) whose combined
                  env: covers every app running on that target, merged into one shared
                  .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
                  that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
                  {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
                  kept from seeing each other's variables today. The prefix convention exists
                  specifically because everything gets poured into one shared canvas first
                  and has to be sorted back out afterward, and it leaks into the compose
                  files themselves: apps/traefik/docker-compose.yml references
                  ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
                  compose files already aren't vanilla-compose-compatible because of this.

                  An earlier version of this issue proposed fixing that by naming vaults
                  after the app they belong to (vaults/traefik.yml) and relying on the
                  filename matching the app name as the link. That's wrong: it reintroduces
                  exactly the kind of implicit, convention-based wiring this repo has
                  consistently avoided elsewhere (explicit app_refs/env_refs, "matching
                  filenames are a convenience, not an implicit relationship" for
                  vault/target pairing). It also doesn't resolve on its own — see below.

                  Proposal

                  Make the target the explicit wiring point between an app and the vault(s)
                  that feed it, the same way it already explicitly wires app_refs and
                  env_refs today — just scoped per app instead of per target:

                  apps:
                  traefik:
                  env_refs:
                  - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
                  env_refs:
                  - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

                  replacing the current flat apps: [traefik, rybbit] list — the app names
                  are just the mapping's keys now, so nothing needed for deploy.sh's
                  APPS= synthesis is lost.

                  Each app's vault can then declare its env output names directly, with zero
                  prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

                  asset: hawkeye-traefik.sops.envkeys:
                  - hawkeyeenv:
                  HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

                  HTTP_PORT is exactly what docker-compose.yml can reference — no
                  prefix stripped anywhere downstream, no hidden rename. Since the vault is
                  referenced explicitly by ref (not discovered by filename), its filename
                  doesn't need to match the app name at all — asset names just need to be
                  unique within a release, same as any other bundle asset.

                  This resolves what the filename-convention version of this issue couldn't:

                  • No asset-naming collision risk across targets. Nothing depends on
                    the vault's filename matching the app's name, so two different targets
                    running the same app never fight over an asset name.
                  • Collisions are detected exactly where they're real, not where they
                    aren't.
                    Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
                    app's own env_refs list. Across different apps there was never a
                    real merge to begin with — a value like DOMAIN duplicated as the same
                    mapping in two apps' vaults is structurally guaranteed to resolve to the
                    same GitHub Secret value; the only way it drifts is a typo in one
                    mapping, which is an ordinary authoring mistake, not a lost safety net.
                  • Cross-repo secret composition falls out for free, no new mechanism
                    needed.
                    This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
                    CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
                    that needs to contribute e.g. a Telegram bot token just needs a copy of
                    the target's public key (keys/hawkeye.pub — not secret, freely
                    copyable to any repo, any org) and to run the already-published
                    encrypt-env action itself in its own release workflow. It publishes
                    its own encrypted asset to its own release. The target then simply adds
                    that ref to whichever app's env_refs needs it:
                    owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
                    env_refs loop already parses the repo out of the ref itself (not
                    hardcoded to flightdeck's own repo) and decrypts with the target's own
                    SOPS_AGE_KEY_FILE regardless of which repo published the asset —
                    decryption is keyed by recipient, not by source repo. Zero code
                    changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
                    superseded by this.

                  Resolved: how per-app env actually reaches the server

                  Settled during discussion with #111 (Ansible→Fabric), then superseded by
                  a bigger decision made in the same implementation session: flightdeck has
                  no manual administration flow at all, ever — the "manual local
                  quick-start" path described below was deleted outright, not kept as a
                  separate untouched path.

                  • Automated path is now the only path. Ref-resolution and downloading
                    run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
                    runner pushes the finished result to the server rather than the server
                    pulling anything.
                  • Decryption and config-template rendering both happen on the runner
                    too
                    (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
                    planned here. The server never runs sops, never holds an age key,
                    never runs envsubst. It receives a real, already-decrypted .env and
                    already-rendered config directly.
                  • Collision detection still happens in CI, from ciphertext, before
                    anything is decrypted or pushed
                    — this part of the design didn't
                    change. SOPS's dotenv output only encrypts values — key names stay in
                    cleartext in the downloaded .sops.env asset itself
                    (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
                    entry, deploy/collisions.py compares key names across the downloaded
                    encrypted files directly, no decryption needed. Any duplicate key fails
                    the build before anything is decrypted.
                  • Net result: no shell scripting left on the server at all.
                    up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                    are all deleted, no replacements. The server's only dependencies are
                    Docker and Docker Compose.

                  Related

                  Builds on #104/#108/#110 (env_refs as a list, apps living on the
                  target). Supersedes and closes #113 (cross-vault import) — same goal,
                  solved by this design without new machinery. Implemented together with
                  #111 (Ansible→Fabric) — this is the concrete feature the push-based
                  model in #111 was designed to carry, not built separately in Ansible.
                  Also affects #103 (ansible ref-resolution dedup), which becomes moot
                  under #111 rather than needing its own fix.

                  Implemented, together with #111 and #116, going further than originally
                  scoped here (see the updated section above).

                  Metadata

                  Metadata

                  Assignees

                  No one assigned

                    Labels

                    No labels
                    No labels

                    Type

                    No type

                    Projects

                    No projects

                      Milestone

                      No milestone

                      Relationships

                      None yet

                      Development

                      No branches or pull requests

                      Issue actions

                      , 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
                      Skip to content

                      One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

                      Description

                      @ineedjet

                      Context

                      Right now a target has one (or more, via env_refs) vault(s) whose combined
                      env: covers every app running on that target, merged into one shared
                      .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
                      that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
                      {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
                      kept from seeing each other's variables today. The prefix convention exists
                      specifically because everything gets poured into one shared canvas first
                      and has to be sorted back out afterward, and it leaks into the compose
                      files themselves: apps/traefik/docker-compose.yml references
                      ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
                      compose files already aren't vanilla-compose-compatible because of this.

                      An earlier version of this issue proposed fixing that by naming vaults
                      after the app they belong to (vaults/traefik.yml) and relying on the
                      filename matching the app name as the link. That's wrong: it reintroduces
                      exactly the kind of implicit, convention-based wiring this repo has
                      consistently avoided elsewhere (explicit app_refs/env_refs, "matching
                      filenames are a convenience, not an implicit relationship" for
                      vault/target pairing). It also doesn't resolve on its own — see below.

                      Proposal

                      Make the target the explicit wiring point between an app and the vault(s)
                      that feed it, the same way it already explicitly wires app_refs and
                      env_refs today — just scoped per app instead of per target:

                      apps:
                      traefik:
                      env_refs:
                      - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
                      env_refs:
                      - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

                      replacing the current flat apps: [traefik, rybbit] list — the app names
                      are just the mapping's keys now, so nothing needed for deploy.sh's
                      APPS= synthesis is lost.

                      Each app's vault can then declare its env output names directly, with zero
                      prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

                      asset: hawkeye-traefik.sops.envkeys:
                      - hawkeyeenv:
                      HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

                      HTTP_PORT is exactly what docker-compose.yml can reference — no
                      prefix stripped anywhere downstream, no hidden rename. Since the vault is
                      referenced explicitly by ref (not discovered by filename), its filename
                      doesn't need to match the app name at all — asset names just need to be
                      unique within a release, same as any other bundle asset.

                      This resolves what the filename-convention version of this issue couldn't:

                      • No asset-naming collision risk across targets. Nothing depends on
                        the vault's filename matching the app's name, so two different targets
                        running the same app never fight over an asset name.
                      • Collisions are detected exactly where they're real, not where they
                        aren't.
                        Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
                        app's own env_refs list. Across different apps there was never a
                        real merge to begin with — a value like DOMAIN duplicated as the same
                        mapping in two apps' vaults is structurally guaranteed to resolve to the
                        same GitHub Secret value; the only way it drifts is a typo in one
                        mapping, which is an ordinary authoring mistake, not a lost safety net.
                      • Cross-repo secret composition falls out for free, no new mechanism
                        needed.
                        This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
                        CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
                        that needs to contribute e.g. a Telegram bot token just needs a copy of
                        the target's public key (keys/hawkeye.pub — not secret, freely
                        copyable to any repo, any org) and to run the already-published
                        encrypt-env action itself in its own release workflow. It publishes
                        its own encrypted asset to its own release. The target then simply adds
                        that ref to whichever app's env_refs needs it:
                        owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
                        env_refs loop already parses the repo out of the ref itself (not
                        hardcoded to flightdeck's own repo) and decrypts with the target's own
                        SOPS_AGE_KEY_FILE regardless of which repo published the asset —
                        decryption is keyed by recipient, not by source repo. Zero code
                        changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
                        superseded by this.

                      Resolved: how per-app env actually reaches the server

                      Settled during discussion with #111 (Ansible→Fabric), then superseded by
                      a bigger decision made in the same implementation session: flightdeck has
                      no manual administration flow at all, ever — the "manual local
                      quick-start" path described below was deleted outright, not kept as a
                      separate untouched path.

                      • Automated path is now the only path. Ref-resolution and downloading
                        run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
                        runner pushes the finished result to the server rather than the server
                        pulling anything.
                      • Decryption and config-template rendering both happen on the runner
                        too
                        (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
                        planned here. The server never runs sops, never holds an age key,
                        never runs envsubst. It receives a real, already-decrypted .env and
                        already-rendered config directly.
                      • Collision detection still happens in CI, from ciphertext, before
                        anything is decrypted or pushed
                        — this part of the design didn't
                        change. SOPS's dotenv output only encrypts values — key names stay in
                        cleartext in the downloaded .sops.env asset itself
                        (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
                        entry, deploy/collisions.py compares key names across the downloaded
                        encrypted files directly, no decryption needed. Any duplicate key fails
                        the build before anything is decrypted.
                      • Net result: no shell scripting left on the server at all.
                        up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                        are all deleted, no replacements. The server's only dependencies are
                        Docker and Docker Compose.

                      Related

                      Builds on #104/#108/#110 (env_refs as a list, apps living on the
                      target). Supersedes and closes #113 (cross-vault import) — same goal,
                      solved by this design without new machinery. Implemented together with
                      #111 (Ansible→Fabric) — this is the concrete feature the push-based
                      model in #111 was designed to carry, not built separately in Ansible.
                      Also affects #103 (ansible ref-resolution dedup), which becomes moot
                      under #111 rather than needing its own fix.

                      Implemented, together with #111 and #116, going further than originally
                      scoped here (see the updated section above).

                      Metadata

                      Metadata

                      Assignees

                      No one assigned

                        Labels

                        No labels
                        No labels

                        Type

                        No type

                        Projects

                        No projects

                          Milestone

                          No milestone

                          Relationships

                          None yet

                          Development

                          No branches or pull requests

                          Issue actions

                          , 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
                          Skip to content

                          One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

                          Description

                          @ineedjet

                          Context

                          Right now a target has one (or more, via env_refs) vault(s) whose combined
                          env: covers every app running on that target, merged into one shared
                          .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
                          that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
                          {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
                          kept from seeing each other's variables today. The prefix convention exists
                          specifically because everything gets poured into one shared canvas first
                          and has to be sorted back out afterward, and it leaks into the compose
                          files themselves: apps/traefik/docker-compose.yml references
                          ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
                          compose files already aren't vanilla-compose-compatible because of this.

                          An earlier version of this issue proposed fixing that by naming vaults
                          after the app they belong to (vaults/traefik.yml) and relying on the
                          filename matching the app name as the link. That's wrong: it reintroduces
                          exactly the kind of implicit, convention-based wiring this repo has
                          consistently avoided elsewhere (explicit app_refs/env_refs, "matching
                          filenames are a convenience, not an implicit relationship" for
                          vault/target pairing). It also doesn't resolve on its own — see below.

                          Proposal

                          Make the target the explicit wiring point between an app and the vault(s)
                          that feed it, the same way it already explicitly wires app_refs and
                          env_refs today — just scoped per app instead of per target:

                          apps:
                          traefik:
                          env_refs:
                          - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
                          env_refs:
                          - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

                          replacing the current flat apps: [traefik, rybbit] list — the app names
                          are just the mapping's keys now, so nothing needed for deploy.sh's
                          APPS= synthesis is lost.

                          Each app's vault can then declare its env output names directly, with zero
                          prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

                          asset: hawkeye-traefik.sops.envkeys:
                          - hawkeyeenv:
                          HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

                          HTTP_PORT is exactly what docker-compose.yml can reference — no
                          prefix stripped anywhere downstream, no hidden rename. Since the vault is
                          referenced explicitly by ref (not discovered by filename), its filename
                          doesn't need to match the app name at all — asset names just need to be
                          unique within a release, same as any other bundle asset.

                          This resolves what the filename-convention version of this issue couldn't:

                          • No asset-naming collision risk across targets. Nothing depends on
                            the vault's filename matching the app's name, so two different targets
                            running the same app never fight over an asset name.
                          • Collisions are detected exactly where they're real, not where they
                            aren't.
                            Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
                            app's own env_refs list. Across different apps there was never a
                            real merge to begin with — a value like DOMAIN duplicated as the same
                            mapping in two apps' vaults is structurally guaranteed to resolve to the
                            same GitHub Secret value; the only way it drifts is a typo in one
                            mapping, which is an ordinary authoring mistake, not a lost safety net.
                          • Cross-repo secret composition falls out for free, no new mechanism
                            needed.
                            This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
                            CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
                            that needs to contribute e.g. a Telegram bot token just needs a copy of
                            the target's public key (keys/hawkeye.pub — not secret, freely
                            copyable to any repo, any org) and to run the already-published
                            encrypt-env action itself in its own release workflow. It publishes
                            its own encrypted asset to its own release. The target then simply adds
                            that ref to whichever app's env_refs needs it:
                            owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
                            env_refs loop already parses the repo out of the ref itself (not
                            hardcoded to flightdeck's own repo) and decrypts with the target's own
                            SOPS_AGE_KEY_FILE regardless of which repo published the asset —
                            decryption is keyed by recipient, not by source repo. Zero code
                            changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
                            superseded by this.

                          Resolved: how per-app env actually reaches the server

                          Settled during discussion with #111 (Ansible→Fabric), then superseded by
                          a bigger decision made in the same implementation session: flightdeck has
                          no manual administration flow at all, ever — the "manual local
                          quick-start" path described below was deleted outright, not kept as a
                          separate untouched path.

                          • Automated path is now the only path. Ref-resolution and downloading
                            run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
                            runner pushes the finished result to the server rather than the server
                            pulling anything.
                          • Decryption and config-template rendering both happen on the runner
                            too
                            (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
                            planned here. The server never runs sops, never holds an age key,
                            never runs envsubst. It receives a real, already-decrypted .env and
                            already-rendered config directly.
                          • Collision detection still happens in CI, from ciphertext, before
                            anything is decrypted or pushed
                            — this part of the design didn't
                            change. SOPS's dotenv output only encrypts values — key names stay in
                            cleartext in the downloaded .sops.env asset itself
                            (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
                            entry, deploy/collisions.py compares key names across the downloaded
                            encrypted files directly, no decryption needed. Any duplicate key fails
                            the build before anything is decrypted.
                          • Net result: no shell scripting left on the server at all.
                            up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                            are all deleted, no replacements. The server's only dependencies are
                            Docker and Docker Compose.

                          Related

                          Builds on #104/#108/#110 (env_refs as a list, apps living on the
                          target). Supersedes and closes #113 (cross-vault import) — same goal,
                          solved by this design without new machinery. Implemented together with
                          #111 (Ansible→Fabric) — this is the concrete feature the push-based
                          model in #111 was designed to carry, not built separately in Ansible.
                          Also affects #103 (ansible ref-resolution dedup), which becomes moot
                          under #111 rather than needing its own fix.

                          Implemented, together with #111 and #116, going further than originally
                          scoped here (see the updated section above).

                          Metadata

                          Metadata

                          Assignees

                          No one assigned

                            Labels

                            No labels
                            No labels

                            Type

                            No type

                            Projects

                            No projects

                              Milestone

                              No milestone

                              Relationships

                              None yet

                              Development

                              No branches or pull requests

                              Issue actions

                              , 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env · Issue #114 · rubykatzen/flightdeck · GitHub
                              Skip to content

                              One vault per app: drop the APPS_/{app}_ prefix convention for vault-sourced env #114

                              Description

                              @ineedjet

                              Context

                              Right now a target has one (or more, via env_refs) vault(s) whose combined
                              env: covers every app running on that target, merged into one shared
                              .env. Downstream, generate-env.sh/lib.sh's generate_env() filters
                              that single canvas per app by name prefix (APP_NAME, APPS_* shared, or
                              {APPNAME}_* own) before writing apps/{app}/.env — this is how apps are
                              kept from seeing each other's variables today. The prefix convention exists
                              specifically because everything gets poured into one shared canvas first
                              and has to be sorted back out afterward, and it leaks into the compose
                              files themselves: apps/traefik/docker-compose.yml references
                              ${TRAEFIK_HTTP_PORT} directly, not a vanilla ${HTTP_PORT} — flightdeck
                              compose files already aren't vanilla-compose-compatible because of this.

                              An earlier version of this issue proposed fixing that by naming vaults
                              after the app they belong to (vaults/traefik.yml) and relying on the
                              filename matching the app name as the link. That's wrong: it reintroduces
                              exactly the kind of implicit, convention-based wiring this repo has
                              consistently avoided elsewhere (explicit app_refs/env_refs, "matching
                              filenames are a convenience, not an implicit relationship" for
                              vault/target pairing). It also doesn't resolve on its own — see below.

                              Proposal

                              Make the target the explicit wiring point between an app and the vault(s)
                              that feed it, the same way it already explicitly wires app_refs and
                              env_refs today — just scoped per app instead of per target:

                              apps:
                              traefik:
                              env_refs:
                              - rubykatzen/flightdeck@latest:hawkeye-traefik.sops.envrybbit:
                              env_refs:
                              - rubykatzen/flightdeck@latest:hawkeye-rybbit.sops.env

                              replacing the current flat apps: [traefik, rybbit] list — the app names
                              are just the mapping's keys now, so nothing needed for deploy.sh's
                              APPS= synthesis is lost.

                              Each app's vault can then declare its env output names directly, with zero
                              prefix and zero indirection — e.g. vaults/hawkeye-traefik.yml:

                              asset: hawkeye-traefik.sops.envkeys:
                              - hawkeyeenv:
                              HTTP_PORT: RUBYKATZEN_COM_TRAEFIK_HTTP_PORTDOMAIN: RUBYKATZEN_COM_DOMAIN

                              HTTP_PORT is exactly what docker-compose.yml can reference — no
                              prefix stripped anywhere downstream, no hidden rename. Since the vault is
                              referenced explicitly by ref (not discovered by filename), its filename
                              doesn't need to match the app name at all — asset names just need to be
                              unique within a release, same as any other bundle asset.

                              This resolves what the filename-convention version of this issue couldn't:

                              • No asset-naming collision risk across targets. Nothing depends on
                                the vault's filename matching the app's name, so two different targets
                                running the same app never fight over an asset name.
                              • Collisions are detected exactly where they're real, not where they
                                aren't.
                                Fail-loud detection (from Support multiple env sources (env_refs) merged per target, like app_refs #104/feat: move apps to targets, support env_refs as a list #110) still applies within one
                                app's own env_refs list. Across different apps there was never a
                                real merge to begin with — a value like DOMAIN duplicated as the same
                                mapping in two apps' vaults is structurally guaranteed to resolve to the
                                same GitHub Secret value; the only way it drifts is a typo in one
                                mapping, which is an ordinary authoring mistake, not a lost safety net.
                              • Cross-repo secret composition falls out for free, no new mechanism
                                needed.
                                This is what made Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 (vault-importing-vault, with its own
                                CI-side decrypt key and encrypt-env changes) unnecessary: a third repo
                                that needs to contribute e.g. a Telegram bot token just needs a copy of
                                the target's public key (keys/hawkeye.pub — not secret, freely
                                copyable to any repo, any org) and to run the already-published
                                encrypt-env action itself in its own release workflow. It publishes
                                its own encrypted asset to its own release. The target then simply adds
                                that ref to whichever app's env_refs needs it:
                                owner3/repo3@latest:telegram.sops.env. ansible/deploy.yml's
                                env_refs loop already parses the repo out of the ref itself (not
                                hardcoded to flightdeck's own repo) and decrypts with the target's own
                                SOPS_AGE_KEY_FILE regardless of which repo published the asset —
                                decryption is keyed by recipient, not by source repo. Zero code
                                changes needed for this to already work today. Closed Let a vault manifest import other vaults by ref (cross-repo secret composition) #113 as
                                superseded by this.

                              Resolved: how per-app env actually reaches the server

                              Settled during discussion with #111 (Ansible→Fabric), then superseded by
                              a bigger decision made in the same implementation session: flightdeck has
                              no manual administration flow at all, ever — the "manual local
                              quick-start" path described below was deleted outright, not kept as a
                              separate untouched path.

                              • Automated path is now the only path. Ref-resolution and downloading
                                run entirely on the CI runner (deploy/deploy.py, see Consider migrating ansible/deploy.yml to Fabric (Python) #111); the CI
                                runner pushes the finished result to the server rather than the server
                                pulling anything.
                              • Decryption and config-template rendering both happen on the runner
                                too
                                (see Consider decrypting vaults on the CI runner instead of server-side #116, decided alongside this) — not server-side as originally
                                planned here. The server never runs sops, never holds an age key,
                                never runs envsubst. It receives a real, already-decrypted .env and
                                already-rendered config directly.
                              • Collision detection still happens in CI, from ciphertext, before
                                anything is decrypted or pushed
                                — this part of the design didn't
                                change. SOPS's dotenv output only encrypts values — key names stay in
                                cleartext in the downloaded .sops.env asset itself
                                (APPS_DOMAIN=ENC[...]). So when an app's env_refs has more than one
                                entry, deploy/collisions.py compares key names across the downloaded
                                encrypted files directly, no decryption needed. Any duplicate key fails
                                the build before anything is decrypted.
                              • Net result: no shell scripting left on the server at all.
                                up.sh/down.sh/restart.sh/deploy.sh/generate-env.sh/lib.sh
                                are all deleted, no replacements. The server's only dependencies are
                                Docker and Docker Compose.

                              Related

                              Builds on #104/#108/#110 (env_refs as a list, apps living on the
                              target). Supersedes and closes #113 (cross-vault import) — same goal,
                              solved by this design without new machinery. Implemented together with
                              #111 (Ansible→Fabric) — this is the concrete feature the push-based
                              model in #111 was designed to carry, not built separately in Ansible.
                              Also affects #103 (ansible ref-resolution dedup), which becomes moot
                              under #111 rather than needing its own fix.

                              Implemented, together with #111 and #116, going further than originally
                              scoped here (see the updated section above).

                              Metadata

                              Metadata

                              Assignees

                              No one assigned

                                Labels

                                No labels
                                No labels

                                Type

                                No type

                                Projects

                                No projects

                                  Milestone

                                  No milestone

                                  Relationships

                                  None yet

                                  Development

                                  No branches or pull requests

                                  Issue actions