feat(releases): Zugangsschutz ueber Lizenzschluessel
/releases/ wurde bisher offen ausgeliefert, damit ausgelieferte Anwendungen ohne Zugangsdaten nach Updates suchen koennen. Das bedeutete aber auch, dass jeder im Internet die vollstaendigen Pakete herunterladen konnte - mitsamt allem, was versehentlich darin liegt. Genau so lag ein echter API-Schluessel in einer mitgelieferten appsettings.json oeffentlich abrufbar. Zugang haengt jetzt am Lizenzschluessel: Wer eine gueltige Lizenz fuer ein Produkt hat, kommt an dessen Updates. Die Anwendung kennt ihren Schluessel ohnehin und versorgt sich damit selbst - es muss nichts verteilt werden. Server - ReleaseGuard erzeugt je Produktverzeichnis .htaccess und .htpasswd. Bewusst getrennt: eine gemeinsame Datei wuerde bedeuten, dass eine Lizenz fuer Produkt A auch Produkt B oeffnet. license_licenses.product_id bindet jeden Schluessel ohnehin an genau ein Projekt. - Eingetragen werden aktive, nicht abgelaufene Lizenzen (Benutzername = Passwort = Schluessel; Basic Auth braucht zwei Felder, es gibt aber nur ein Geheimnis) sowie alle Installationskonten - bei einer Erstinstallation gibt es noch keinen Schluessel, mit dem sich das Paket holen liesse. - Deren Hash wird unveraendert aus dc_users uebernommen: password_hash() erzeugt bcrypt im Format $2y$, genau das versteht Apache. Ein Klartextpasswort wird nirgends gebraucht. Argon2-Hashes werden erkannt und uebersprungen statt eine unbrauchbare Datei zu erzeugen. - Lizenzschluessel werden mit Kosten 8 gehasht statt 12: 29 Zeichen maschineller Zufall sind kein Menschenpasswort, Apache prueft aber bei *jeder* Anfrage neu. - Geschrieben wird ueber eine temporaere Datei mit rename() - ein Abbruch wuerde sonst eine halbe Zugangsdatei hinterlassen und in dem Moment die halbe Kundschaft aussperren. - Neu erzeugt bei jeder Lizenz- und Kontoaenderung. Abgelaufene Lizenzen loesen anders als ein Widerruf nichts aus; dafuer gleicht cli/tick.php nach und erzeugt spaetestens alle sechs Stunden neu. - Statusanzeige und Schaltflaeche im WebUI unter UpdateService. Client - ReleaseCredentials: Lizenzschluessel oder Installationskonto als Basic Auth. - UpdateClient und Agent senden sie fuer latest.json und package.tar.gz. - UpdateCheckResult.Unauthorized trennt "Lizenz traegt nicht mehr" von einem Netzwerkfehler. Ohne diese Unterscheidung sucht man an der falschen Stelle. - LaunchUpdateAgent reicht licenseKey als --license-key durch. - Der Installer benutzt die beim Anmelden eingegebenen Zugangsdaten auch fuer den Paketabruf; das Setup-Token taugt dafuer nicht, weil Apache prueft und nicht die Anwendung. Sonstiges - deploy.py klammert artifacts/ aus. Ohne das landeten die gebauten Installer-Binaries zusaetzlich unter /artifacts/ im Webroot. ACHTUNG Reihenfolge: Der Schutz sperrt jede Anwendung aus, die noch mit dem alten SDK gebaut ist. Erst ausliefern, dann scharfschalten - siehe UPGRADE.md §16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f41372e5f
commit
ceb977187e
@@ -58,6 +58,13 @@ namespace Deploymentcenter.UpdateAgent
|
||||
try
|
||||
{
|
||||
session = await client.LoginAsync(username, password, Environment.MachineName);
|
||||
|
||||
// Dieselben Zugangsdaten oeffnen die Release-Ablage. Das
|
||||
// Setup-Token taugt dafuer nicht: /releases/ liegt hinter
|
||||
// HTTP-Basic-Auth und wird von Apache geprueft, nicht von
|
||||
// der Anwendung. Bei einer Erstinstallation gibt es zudem
|
||||
// noch keinen Lizenzschluessel.
|
||||
options.Credentials = ReleaseCredentials.FromUser(username, password);
|
||||
break;
|
||||
}
|
||||
catch (SetupException ex)
|
||||
@@ -230,6 +237,7 @@ namespace Deploymentcenter.UpdateAgent
|
||||
try
|
||||
{
|
||||
await client.LoginAsync(username, password, Environment.MachineName);
|
||||
options.Credentials = ReleaseCredentials.FromUser(username, password);
|
||||
}
|
||||
catch (SetupException ex)
|
||||
{
|
||||
|
||||
@@ -67,7 +67,16 @@ namespace Deploymentcenter.UpdateAgent
|
||||
PublicKeyPath = GetArg(args, "--pubkey"),
|
||||
AgentDirectory = agentDir,
|
||||
WaitForPid = ParsePid(GetArg(args, "--wait-for-pid")),
|
||||
WaitTimeoutSeconds = ParseInt(GetArg(args, "--wait-timeout"), 60)
|
||||
WaitTimeoutSeconds = ParseInt(GetArg(args, "--wait-timeout"), 60),
|
||||
|
||||
// Die Release-Ablage verlangt Zugangsdaten. Im laufenden
|
||||
// Betrieb ist das der Lizenzschluessel der Anwendung; bei
|
||||
// einer Erstinstallation gibt es den noch nicht, dort treten
|
||||
// die Zugangsdaten des Installationskontos an seine Stelle.
|
||||
Credentials = ReleaseCredentials.FromLicenseKey(GetArg(args, "--license-key"))
|
||||
?? ReleaseCredentials.FromUser(
|
||||
GetArg(args, "--download-user"),
|
||||
GetArg(args, "--download-password"))
|
||||
};
|
||||
|
||||
PackageManifest? installedManifest = ReadInstalledManifest(targetDir);
|
||||
@@ -249,7 +258,14 @@ namespace Deploymentcenter.UpdateAgent
|
||||
{
|
||||
var updateClient = new UpdateClient(HttpClient);
|
||||
var res = await updateClient.CheckForUpdateAsync(
|
||||
options.BaseUrl, options.Project, currentVer, options.Channel, options.Platform);
|
||||
options.BaseUrl, options.Project, currentVer, options.Channel,
|
||||
options.Platform, options.Credentials);
|
||||
|
||||
if (res.Unauthorized)
|
||||
{
|
||||
Console.WriteLine("UNAUTHORIZED: " + res.Message);
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (res.UpdateAvailable && res.LatestRelease != null)
|
||||
{
|
||||
@@ -350,7 +366,18 @@ namespace Deploymentcenter.UpdateAgent
|
||||
.StartAsync(async ctx =>
|
||||
{
|
||||
var downloadTask = ctx.AddTask($"[green]Lade Paket v{targetRelease.Version} ...[/]");
|
||||
using var resp = await HttpClient.GetAsync(pkgUrl, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
using var pkgRequest = new HttpRequestMessage(HttpMethod.Get, pkgUrl);
|
||||
ReleaseCredentials.Apply(pkgRequest, options.Credentials);
|
||||
|
||||
using var resp = await HttpClient.SendAsync(pkgRequest, HttpCompletionOption.ResponseHeadersRead);
|
||||
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
throw new UnauthorizedAccessException(
|
||||
ReleaseCredentials.DescribeUnauthorized(options.Credentials));
|
||||
}
|
||||
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
long? totalBytes = resp.Content.Headers.ContentLength;
|
||||
@@ -562,7 +589,22 @@ namespace Deploymentcenter.UpdateAgent
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = await HttpClient.GetAsync(url);
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
ReleaseCredentials.Apply(request, options.Credentials);
|
||||
|
||||
var resp = await HttpClient.SendAsync(request);
|
||||
|
||||
// Ein 401 ist keine "Datei fehlt"-Lage: die Lizenz traegt
|
||||
// nicht mehr. Weiterzuprobieren wuerde die Ursache nur
|
||||
// hinter einer allgemeinen Fehlermeldung verstecken.
|
||||
if (resp.StatusCode == System.Net.HttpStatusCode.Unauthorized)
|
||||
{
|
||||
AnsiConsole.MarkupLine("[bold red]"
|
||||
+ Markup.Escape(ReleaseCredentials.DescribeUnauthorized(options.Credentials))
|
||||
+ "[/]");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!resp.IsSuccessStatusCode)
|
||||
continue;
|
||||
|
||||
@@ -1071,6 +1113,9 @@ namespace Deploymentcenter.UpdateAgent
|
||||
Console.WriteLine(" --wait-for-pid <pid> Vor dem Anwenden auf das Ende dieses Prozesses warten");
|
||||
Console.WriteLine(" --wait-timeout <sek> Geduld dabei (Vorgabe: 60)");
|
||||
Console.WriteLine(" --restart <datei> Nach dem Update zu startende Anwendung");
|
||||
Console.WriteLine(" --license-key <schluessel> Zugang zur Release-Ablage (Regelfall im Betrieb)");
|
||||
Console.WriteLine(" --download-user <benutzer> Alternativ: Installationskonto");
|
||||
Console.WriteLine(" --download-password <pw> dazugehoeriges Passwort");
|
||||
Console.WriteLine(" --pubkey <datei> Oeffentlicher Schluessel zur Signaturpruefung");
|
||||
Console.WriteLine(" --require-signature Ohne gueltige Signatur nicht installieren");
|
||||
Console.WriteLine();
|
||||
@@ -1092,5 +1137,11 @@ namespace Deploymentcenter.UpdateAgent
|
||||
public string AgentDirectory { get; set; } = string.Empty;
|
||||
public int WaitForPid { get; set; }
|
||||
public int WaitTimeoutSeconds { get; set; } = 60;
|
||||
|
||||
/// <summary>
|
||||
/// Zugangsdaten fuer /releases/. Null bedeutet: ohne Anmeldung
|
||||
/// versuchen - eine noch ungeschuetzte Ablage bleibt so erreichbar.
|
||||
/// </summary>
|
||||
public ReleaseCredentials? Credentials { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user