Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package-firewall/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ Both the bash and PowerShell generators read block content from `shared/blocks/`

```
shared/blocks/
├── envsh.txt ← ~/.config/endor/env.sh content (bash only)
├── npmrc.txt ← .npmrc content
├── yarnrc_classic.txt ← .yarnrc content (yarn 1.x)
├── yarnrc.txt ← .yarnrc.yml content (yarn 2+)
Expand All @@ -46,6 +45,8 @@ shared/blocks/

Edit these files to customise what gets written to developer machines. The orchestration scripts (`templates/*.sh` / `templates/*.ps1`) control which files get written and in what order.

> **User attribution:** the `${ENDOR_*}` credential values referenced by these blocks (e.g. `${ENDOR_ATTR_USER}`, `${ENDOR_AUTH_B64}`) are computed **at install time** on each developer's machine — see `bash/templates/credentials.sh` and `powershell/templates/envvars.ps1`. This stamps `<console-user>@<machine>` onto each firewall request (shown as **User** in the log) without per-user API keys. `~/.config/endor/env.sh` is generated from those values by `bash/templates/envsh.sh`.

---

## Generated output
Expand Down
4 changes: 2 additions & 2 deletions package-firewall/bash/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ bash/
└── endor-remove.sh

../shared/blocks/ ← edit these to customise what gets written to config files
├── envsh.txt ← ~/.config/endor/env.sh content
├── npmrc.txt ← ~/.npmrc content
├── yarnrc_classic.txt ← ~/.yarnrc (yarn 1.x) content
├── yarnrc.txt ← ~/.yarnrc.yml (yarn 2+) content
Expand Down Expand Up @@ -203,7 +202,6 @@ To change what gets written to a config file on target machines, edit the releva

| File | Written to |
|---|---|
| `../shared/blocks/envsh.txt` | `~/.config/endor/env.sh` |
| `../shared/blocks/npmrc.txt` | `~/.npmrc` |
| `../shared/blocks/yarnrc_classic.txt` | `~/.yarnrc` (yarn 1.x) |
| `../shared/blocks/yarnrc.txt` | `~/.yarnrc.yml` (yarn 2+) |
Expand All @@ -212,6 +210,8 @@ To change what gets written to a config file on target machines, edit the releva
| `../shared/blocks/goenv.txt` | `~/.config/go/env` |
| `../shared/blocks/mavensettings.txt` | `~/.m2/settings.xml` |

`~/.config/endor/env.sh` is no longer a static block — it is generated by `templates/envsh.sh` from the user-attribution credentials computed at install time in `templates/credentials.sh` (which derives `${ENDOR_ATTR_USER}` etc. from `<console-user>@<machine>`).

To change orchestration logic (which files get written, in what order, with what warnings), edit the relevant `templates/*.sh` file directly.

Both support `{{PLACEHOLDER}}` substitution at generation time and `${ENDOR_VAR}` env var references at runtime:
Expand Down
36 changes: 22 additions & 14 deletions package-firewall/bash/generate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,26 @@ SHARED_BLOCKS_DIR="$SCRIPT_DIR/../shared/blocks"
FQDN="${ENDOR_FQDN:-https://factory.endorlabs.com}"

# ─── Compute derived values ────────────────────────────────────────────────────
# Credentials are NOT precomputed here. The per-machine attributed username
# (<console-user>@<machine>) only exists on the developer's machine, so all auth
# values are computed at install time by templates/credentials.sh. Only credential-free
# values (hosts, registry URLs) are derived here.
FQDN_HOST="${FQDN#https://}"
FQDN_HOST="${FQDN_HOST#http://}"
TRUSTED_HOST="${FQDN_HOST%%:*}"

NPM_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/"
NPM_REGISTRY_HOST="${FQDN_HOST}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/npm/"
NPM_AUTH_B64=$(printf '%s' "${ENDOR_API_KEY_ID}:${ENDOR_API_SECRET}" | base64 | tr -d '\n')
API_SECRET_B64=$(printf '%s' "${ENDOR_API_SECRET}" | base64 | tr -d '\n')

PYPI_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/pypi/simple/"
PIP_INDEX_URL="https://${ENDOR_API_KEY_ID}:${ENDOR_API_SECRET}@${FQDN_HOST}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/pypi/simple/"

GO_PROXY_URL="https://${ENDOR_API_KEY_ID}:${ENDOR_API_SECRET}@${FQDN_HOST}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/go/,direct"
MAVEN_REGISTRY_URL="${FQDN}/v1/namespaces/${ENDOR_NAMESPACE}/firewall/maven/"

# npm rejects self-signed TLS by default; relax it only for a localhost firewall
# (local testing). Empty for any real (staging/prod) host.
case "$TRUSTED_HOST" in
localhost|127.0.0.1|::1) NPM_STRICT_SSL="strict-ssl=false" ;;
*) NPM_STRICT_SSL="" ;;
esac

# ─── Output directory ─────────────────────────────────────────────────────────
OUT_DIR="${SCRIPT_DIR}/out/${ENDOR_NAMESPACE}"
mkdir -p "$OUT_DIR"
Expand All @@ -75,15 +80,12 @@ substitute() {
-e "s|{{API_KEY_ID}}|${ENDOR_API_KEY_ID}|g" \
-e "s|{{API_SECRET}}|${ENDOR_API_SECRET}|g" \
-e "s|{{FQDN}}|${FQDN}|g" \
-e "s|{{FQDN_HOST}}|${FQDN_HOST}|g" \
-e "s|{{NPM_REGISTRY_URL}}|${NPM_REGISTRY_URL}|g" \
-e "s|{{NPM_REGISTRY_HOST}}|${NPM_REGISTRY_HOST}|g" \
-e "s|{{NPM_AUTH_B64}}|${NPM_AUTH_B64}|g" \
-e "s|{{API_SECRET_B64}}|${API_SECRET_B64}|g" \
-e "s|{{NPM_STRICT_SSL}}|${NPM_STRICT_SSL}|g" \
-e "s|{{PYPI_URL}}|${PYPI_URL}|g" \
-e "s|{{PIP_INDEX_URL}}|${PIP_INDEX_URL}|g" \
-e "s|{{ENDOR_PYPI_URL}}|${PIP_INDEX_URL}|g" \
-e "s|{{TRUSTED_HOST}}|${TRUSTED_HOST}|g" \
-e "s|{{GO_PROXY_URL}}|${GO_PROXY_URL}|g" \
-e "s|{{MAVEN_REGISTRY_URL}}|${MAVEN_REGISTRY_URL}|g"
}

Expand Down Expand Up @@ -111,10 +113,10 @@ emit_block_assignment() {
# emit_all_blocks
# Emits all block variable assignments into the generated script.
# Edit shared/blocks/*.txt to change shared config content.
# Edit templates/envsh.txt to change the bash-only env var block.
# The env.sh block is built at runtime by templates/envsh.sh, from the
# attribution credentials computed in templates/credentials.sh.
emit_all_blocks() {
echo "# ── Block content (from shared/blocks/) ─────────────────────────────────────"
emit_block_assignment "ENVSH_BLOCK" "$SHARED_BLOCKS_DIR/envsh.txt"
emit_block_assignment "NPMRC_BLOCK" "$SHARED_BLOCKS_DIR/npmrc.txt"
emit_block_assignment "YARNRC_CLASSIC_BLOCK" "$SHARED_BLOCKS_DIR/yarnrc_classic.txt"
emit_block_assignment "YARNRC_BLOCK" "$SHARED_BLOCKS_DIR/yarnrc.txt"
Expand Down Expand Up @@ -192,6 +194,9 @@ build_script() {

{
script_header "$output" "$description"
echo "# ── User attribution (credentials computed at install time) ──────────────────"
substitute < "$TMPL_DIR/credentials.sh"
echo ""
emit_all_blocks
echo "# ════════════════════════════════════════════════════════════════════════════"
echo "# Env setup"
Expand Down Expand Up @@ -247,6 +252,9 @@ build_remove_script "$OUT_DIR/endor-remove.sh"
{
script_header "$OUT_DIR/endor-all.sh" \
"Configures all package managers for Endor Package Firewall. Covers: npm · pnpm · yarn classic · yarn 2+ · bun · pip · uv · poetry · go · maven"
echo "# ── User attribution (credentials computed at install time) ──────────────────"
substitute < "$TMPL_DIR/credentials.sh"
echo ""
emit_all_blocks
echo "# ════════════════════════════════════════════════════════════════════════════"
echo "# Env setup"
Expand Down Expand Up @@ -295,7 +303,7 @@ echo " All scripts accept --dry-run to preview changes without writing anythin
echo " Upload to your MDM tool. Each script is self-contained and idempotent."
echo ""
echo " To customise: edit shared/blocks/*.txt (shared config content)"
echo " or templates/envsh.txt (bash env var block)"
echo " or templates/credentials.sh (user-attribution credential block)"
echo " or templates/*.sh (orchestration logic)"
echo ""
echo " Re-running overwrites the same output directory."
39 changes: 39 additions & 0 deletions package-firewall/bash/lib/common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,45 @@ ENDOR_BLOCK_END="# ===== END ENDOR PACKAGE FIREWALL ====="
ENDOR_XML_BLOCK_START="<!-- ===== BEGIN ENDOR PACKAGE FIREWALL (managed — do not edit) ===== -->"
ENDOR_XML_BLOCK_END="<!-- ===== END ENDOR PACKAGE FIREWALL ===== -->"

# ── User attribution helpers ──────────────────────────────────────────────────
# These let the generated scripts stamp <console-user>@<machine> onto each
# package-firewall request WITHOUT issuing per-user API keys. The label is encoded
# into the Basic-auth username; the firewall decodes it, authenticates with the
# real shared API key, and records the label on the firewall log (shown as "User").
# The label is client-supplied and UNVERIFIED — telemetry only, never an authz signal.

# endor_b64 — portable base64 with no line wrapping (reads stdin).
# GNU coreutils wraps at 76 cols unless -w0; BSD/macOS prints one line.
endor_b64() {
if base64 --help 2>&1 | grep -q -- '-w'; then
base64 -w0
else
base64 | tr -d '\n'
fi
}

# endor_urlenc_b64 <b64> — percent-encode a base64 string for use in URL userinfo.
# '+', '/' and '=' would otherwise break URL parsing; the tool decodes userinfo
# before building the Basic header, so the firewall still receives the raw base64.
endor_urlenc_b64() {
printf '%s' "$1" | sed -e 's/+/%2B/g' -e 's#/#%2F#g' -e 's/=/%3D/g'
}

# endor_host_label — a stable, human-readable machine name for attribution.
endor_host_label() {
scutil --get ComputerName 2>/dev/null || hostname 2>/dev/null || echo unknown
}

# endor_attr_username <label> <api_key_id>
# Builds the attributed Basic-auth username: base64(base64("userattr:"+label)+":"+keyId).
# The double base64 lets the label contain any characters (':', spaces, '@') safely.
# Mirrors decodeAttributedUsername() in endorfactory's auth layer.
endor_attr_username() {
local label="$1" key_id="$2" inner
inner=$(printf '%s' "userattr:${label}" | endor_b64)
printf '%s:%s' "$inner" "$key_id" | endor_b64
}

# detect_console_user
# MDM tools (Kandji, Jamf) run scripts as root. $HOME resolves to /var/root, which
# is not where developer config files live. Returns the name of the actual logged-in
Expand Down
34 changes: 34 additions & 0 deletions package-firewall/bash/templates/credentials.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# templates/credentials.sh
# User attribution — runs on the developer's machine (where MDM executes this
# script), AFTER the console user has been detected. Computes the attributed
# Basic-auth username so the Package Firewall log can attribute installs to
# <console-user>@<machine> without issuing per-user API keys.
#
# Username = base64( base64("userattr:"+label) + ":" + apiKeyId ); the password is
# the real api secret, unchanged. The firewall decodes the label, authenticates
# with the real key, and stamps the label on the log. UNVERIFIED telemetry only.
#
# The api key id / secret and host / namespace are substituted at generation time;
# everything else is derived here, at runtime, from the per-machine label.

ENDOR_API_KEY_ID='{{API_KEY_ID}}'
ENDOR_API_SECRET='{{API_SECRET}}'

ENDOR_ATTR_LABEL="${CONSOLE_USER}@$(endor_host_label)"
ENDOR_ATTR_USER="$(endor_attr_username "$ENDOR_ATTR_LABEL" "$ENDOR_API_KEY_ID")"

# npm _auth = base64(username:password); _password = base64(password).
ENDOR_AUTH_B64="$(printf '%s:%s' "$ENDOR_ATTR_USER" "$ENDOR_API_SECRET" | endor_b64)"
ENDOR_API_SECRET_B64="$(printf '%s' "$ENDOR_API_SECRET" | endor_b64)"

ENDOR_NPM_REGISTRY_URL='{{NPM_REGISTRY_URL}}'

# pip / uv / go embed the attributed username in URL userinfo — percent-encode it.
ENDOR_PYPI_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):${ENDOR_API_SECRET}@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/pypi/simple/"
ENDOR_GO_PROXY_URL="https://$(endor_urlenc_b64 "$ENDOR_ATTR_USER"):${ENDOR_API_SECRET}@{{FQDN_HOST}}/v1/namespaces/{{NAMESPACE}}/firewall/go/,direct"

export ENDOR_API_KEY_ID ENDOR_API_SECRET ENDOR_ATTR_LABEL ENDOR_ATTR_USER \
ENDOR_AUTH_B64 ENDOR_API_SECRET_B64 ENDOR_NPM_REGISTRY_URL \
ENDOR_PYPI_URL ENDOR_GO_PROXY_URL

echo "[endor] user attribution → ${ENDOR_ATTR_LABEL}"
26 changes: 22 additions & 4 deletions package-firewall/bash/templates/envsh.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
# templates/envsh.sh
# Writes ~/.config/endor/env.sh — the single credential source for all
# env-var-based tools (npm, uv, yarn 2+, poetry; Go in future).
# env-var-based tools (npm, yarn 2+, maven, poetry).
# Then adds a one-line source directive to existing shell profiles.
#
# Block content is defined in templates/blocks/envsh.txt.
# pip is intentionally excluded — pip.conf does not support env var expansion,
# so pip credentials are written as literal values in python.sh instead.
# The values come from templates/credentials.sh, which computes the attributed
# Basic-auth username (<console-user>@<machine>) at install time. They are written
# here as resolved literals so the file is a plain set of exports — no per-shell
# computation on every startup.
#
# pip / uv / go are intentionally NOT sourced from here — those tools cannot expand
# ${VAR}, so their config files get the literal value written by python.sh / go.sh.

echo ""
echo "[endor] ── env.sh setup ─────────────────────────────────────────────────────"

ENDOR_ENV_SH="$USER_HOME/.config/endor/env.sh"

ENVSH_BLOCK=$(cat <<EOF
export ENDOR_API_KEY_ID="$ENDOR_API_KEY_ID"
export ENDOR_API_SECRET="$ENDOR_API_SECRET"
export ENDOR_ATTR_USER="$ENDOR_ATTR_USER"
export ENDOR_AUTH_B64="$ENDOR_AUTH_B64"
export ENDOR_API_SECRET_B64="$ENDOR_API_SECRET_B64"
export ENDOR_NPM_REGISTRY_URL="$ENDOR_NPM_REGISTRY_URL"
export ENDOR_PYPI_URL="$ENDOR_PYPI_URL"
export ENDOR_GO_PROXY_URL="$ENDOR_GO_PROXY_URL"
export POETRY_HTTP_BASIC_ENDOR_FIREWALL_USERNAME="$ENDOR_ATTR_USER"
export POETRY_HTTP_BASIC_ENDOR_FIREWALL_PASSWORD="$ENDOR_API_SECRET"
EOF
)

upsert_block \
"$ENDOR_ENV_SH" \
"$ENVSH_BLOCK" \
Expand Down
8 changes: 6 additions & 2 deletions package-firewall/bash/templates/go.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
# Linux → ~/.config/go/env (or $XDG_CONFIG_HOME/go/env)
#
# Block content is defined in shared/blocks/goenv.txt.
# {{GO_PROXY_URL}} is substituted at generation time — Go env files do not support
# env var expansion, so credentials are baked into the GOPROXY value.
# Go env files do not support env var expansion, so the GOPROXY credential — which
# carries the per-machine attributed username — is baked in as a literal here, by
# resolving the ${ENDOR_GO_PROXY_URL} placeholder computed in credentials.sh.
#
# The go env file takes lower precedence than the GOPROXY process env var, so
# project-level overrides (go env -w GOPROXY=... in a workspace) remain possible.
Expand All @@ -17,6 +18,9 @@
echo ""
echo "[endor-go] ── Go package manager ───────────────────────────────────────────"

# Resolve the attributed GOPROXY into the block content (go env can't expand ${VAR}).
GO_BLOCK=${GO_BLOCK//'${ENDOR_GO_PROXY_URL}'/$ENDOR_GO_PROXY_URL}

# ── Resolve go env file path ──────────────────────────────────────────────────
# Run `go env GOENV` with the user's HOME so Go's os.UserConfigDir() returns
# the correct user-specific path (macOS: ~/Library/Application Support/go/env,
Expand Down
11 changes: 9 additions & 2 deletions package-firewall/bash/templates/python.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@
# Block content is defined in templates/blocks/pipconf.txt and uvtoml.txt.
#
# Credential approach per tool:
# pip → literal credentials in pip.conf (pip cannot expand env vars — see pipconf.txt)
# uv → ${ENDOR_PYPI_URL} env var reference in uv.toml (uv expands ${VAR})
# pip → literal index-url in pip.conf (pip cannot expand env vars)
# uv → literal index-url in uv.toml (baked at install time, like pip)
# poetry → POETRY_HTTP_BASIC_ENDOR_FIREWALL_* env vars via env.sh (written by env.sh step)
#
# pip/uv credentials carry the per-machine attributed username, so they must be the
# literal value computed in credentials.sh — resolve the ${ENDOR_PYPI_URL} placeholder now.
#
# pip section uses [endor-firewall] named section — avoids clobbering admin's [global].

echo ""
echo "[endor-python] ── Python package managers ──────────────────────────────────"

# Resolve the attributed index-url into the block content (pip/uv can't expand ${VAR}).
PIP_BLOCK=${PIP_BLOCK//'${ENDOR_PYPI_URL}'/$ENDOR_PYPI_URL}
UV_BLOCK=${UV_BLOCK//'${ENDOR_PYPI_URL}'/$ENDOR_PYPI_URL}

# ── pip ───────────────────────────────────────────────────────────────────────
# pip reads pip.conf automatically from all three locations below.
# pip does not support env var expansion — credentials are literal values (see pipconf.txt).
Expand Down
Loading