Header-only control token, masked secrets, loopback default (v0.9.9)
Finishes the small items of the security audit in docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7. The three mutating endpoints of BuildsApiController no longer accept the control token as a `token` query parameter — only the X-GitTally-Token header, which the bundled UI has always used. URLs end up in access logs, proxy logs, browser history and Referer headers, and the token never expires, so a historical log capture would yield a valid credential. `config:print` masks git.token as `***` on both the raw and the --full path and names the new --show-secrets flag in a leading YAML comment, so the output stays parseable when piped. The setup script points at --show-secrets where it used to steer the operator to the plain token. `server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the API authenticates read access, so reaching GitTally should require the host's reverse proxy. Existing .gittally.yml files keep their explicit value; the managed nginx container needs `0.0.0.0` set deliberately, which is noted in the release notes, docs/configuration.md and docs/deployment.md. Released as v0.9.9, which also carries the previous two commits. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -17,16 +17,41 @@ class ConfigPrintCommand(
|
||||
@Option(names = ["--full"], description = ["Include all defaults"])
|
||||
var full: Boolean = false
|
||||
|
||||
@Option(names = ["--show-secrets"], description = ["Print secrets (git.token) in clear text instead of masked"])
|
||||
var showSecrets: Boolean = false
|
||||
|
||||
override fun run() {
|
||||
if (full) {
|
||||
print(configLoader.toYaml(configLoader.load()))
|
||||
val config = configLoader.load()
|
||||
printMaskingNote(config.git.token)
|
||||
print(configLoader.toYaml(if (showSecrets) config else config.copy(git = config.git.copy(token = MASK))))
|
||||
} else {
|
||||
val raw = configLoader.loadRaw()
|
||||
if (raw.isEmpty()) {
|
||||
println("(no configuration files found)")
|
||||
} else {
|
||||
print(configLoader.toYaml(raw))
|
||||
printMaskingNote(rawToken(raw))
|
||||
print(configLoader.toYaml(if (showSecrets) raw else maskToken(raw)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A YAML comment, so the output stays parseable when piped into a file. */
|
||||
private fun printMaskingNote(token: String?) {
|
||||
if (!showSecrets && !token.isNullOrEmpty()) {
|
||||
println("# git.token is masked — pass --show-secrets to print it")
|
||||
}
|
||||
}
|
||||
|
||||
private fun rawToken(raw: Map<String, Any?>): String? = (raw["git"] as? Map<*, *>)?.get("token") as? String
|
||||
|
||||
private fun maskToken(raw: Map<String, Any?>): Map<String, Any?> {
|
||||
val git = raw["git"] as? Map<*, *> ?: return raw
|
||||
if (rawToken(raw).isNullOrEmpty()) return raw
|
||||
return raw + ("git" to git.entries.associate { (key, value) -> key to if (key == "token") MASK else value })
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MASK = "***"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,8 +125,10 @@ class InitCommand(
|
||||
publicBaseUrl: ""
|
||||
# HTTP port of the `server` subcommand
|
||||
port: 18080
|
||||
# bind address of the `server` subcommand
|
||||
bindAddress: 0.0.0.0
|
||||
# bind address of the `server` subcommand; loopback only, because the UI and the
|
||||
# API are unauthenticated — use 0.0.0.0 only without a reverse proxy in front
|
||||
# (and with the managed nginx below, which reaches GitTally from its container)
|
||||
bindAddress: 127.0.0.1
|
||||
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
||||
impressumUrl: ""
|
||||
# Opt-in managed nginx+certbot Docker container for HTTPS, for hosts without
|
||||
|
||||
Reference in New Issue
Block a user