The stock SearXNG image ships a native kagi engine; enabling it needs an
API key that must never enter git, and SearXNG does not expand env vars
in settings.yml (the live secret_key literally was '${SEARXNG_SECRET}'
until now — the app tolerated the placeholder string silently).
- settings.yml: kagi engine enabled (weight 2, kg shortcut, paging off so
exactly one API credit per aggregated query, timeout 8.0 vs 5.0 scrapers
so a slow paid query is not a wasted credit); api_key stays ${KAGI_API_KEY}.
- new scripts/searxng-render-settings.py: deploy-time renderer, allowlisted
to KAGI_API_KEY/SEARXNG_SECRET/SEARXNG_BASE_URL, fails the deploy loudly
on an unresolvable placeholder, idempotent on redeploys.
- deploy-remote.sh copy_searxng_config runs the renderer against
/opt/searxng/.env after copying settings.yml into /opt/searxng/config.
- KAGI_API_KEY placeholder added to dankclaw/.env.example (real key staged
into the host-side /opt/searxng/.env, mode 600, out of band).
- bats: engine wiring, renderer substitution, loud-failure, deployer seam.
- docs: SearXNG.md Kagi section + renderer law; Providers.md carve-out on
the no-credentials policy (paid engine acceptable when bounded + ordered).
Verified live on dankclaw: direct API probe via the gluetun netns (200,
data.search[] shape), then an aggregated query returned 29/29 results
tagged kagi, zero unresponsive engines.
75 lines
2.5 KiB
Python
Executable File
75 lines
2.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Render ${VAR} placeholders in the live SearXNG settings.yml from the
|
|
host-side env file.
|
|
|
|
Why this exists: SearXNG does NOT expand environment variables inside
|
|
settings.yml. The repo-tracked shared/searxng/settings.yml deliberately keeps
|
|
secrets as "${KAGI_API_KEY}"-style placeholders so no key ever enters git; the
|
|
deployer copies that file to /opt/searxng/config/settings.yml and runs this
|
|
script to bake in the real values. Without it the app runs with the literal
|
|
placeholder strings (observed 2026-09-26: secret_key literally
|
|
"${SEARXNG_SECRET}" and the Kagi engine failing setup() on an empty key).
|
|
|
|
Only the allowlisted names are substituted. A placeholder that has no value in
|
|
the env file is a deploy-blocking error: serving a paid-API engine with a
|
|
literal "${KAGI_API_KEY}" bearer would waste requests and hide the misconfig.
|
|
|
|
Usage: searxng-render-settings.py ENV_FILE SETTINGS_FILE
|
|
Both paths are taken as arguments; secrets never appear in any argv.
|
|
Exit codes: 0 rendered (idempotent on an already-rendered file), 1 unresolved
|
|
placeholder, 2 usage error.
|
|
"""
|
|
|
|
import sys
|
|
|
|
ALLOWED = ("KAGI_API_KEY", "SEARXNG_SECRET", "SEARXNG_BASE_URL")
|
|
|
|
|
|
def load_env(path: str) -> dict[str, str]:
|
|
values: dict[str, str] = {}
|
|
with open(path, encoding="utf-8") as fh:
|
|
for line in fh:
|
|
line = line.strip()
|
|
if not line or line.startswith("#") or "=" not in line:
|
|
continue
|
|
key, _, value = line.partition("=")
|
|
if key in ALLOWED:
|
|
values[key] = value
|
|
return values
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
args = [a for a in argv[1:]]
|
|
if len(args) != 2:
|
|
print(
|
|
"usage: searxng-render-settings.py ENV_FILE SETTINGS_FILE",
|
|
file=sys.stderr,
|
|
)
|
|
return 2
|
|
env_file, settings_file = args
|
|
|
|
values = load_env(env_file)
|
|
with open(settings_file, encoding="utf-8") as fh:
|
|
text = fh.read()
|
|
|
|
for name in ALLOWED:
|
|
if name in values:
|
|
text = text.replace("${%s}" % name, values[name])
|
|
|
|
unresolved = [n for n in ALLOWED if "${%s}" % n in text]
|
|
if unresolved:
|
|
print(
|
|
"unresolved placeholders in %s: %s (missing from %s)"
|
|
% (settings_file, ", ".join(unresolved), env_file),
|
|
file=sys.stderr,
|
|
)
|
|
return 1
|
|
|
|
with open(settings_file, "w", encoding="utf-8") as fh:
|
|
fh.write(text)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv))
|