feat: harden control-plane deployment
Build web service image / build (push) Successful in 48s

This commit is contained in:
2026-09-12 11:09:42 +08:00
parent 13c31fc902
commit 0d29b828bb
22 changed files with 1213 additions and 71 deletions
+56 -5
View File
@@ -64,6 +64,18 @@ public sealed record RemoteAgentOptions
[JsonPropertyName("token")]
public string? Token { get; init; }
[JsonPropertyName("tokenFile")]
public string? TokenFile { get; init; }
[JsonPropertyName("serverCaFile")]
public string? ServerCaFile { get; init; }
[JsonPropertyName("clientCertificateFile")]
public string? ClientCertificateFile { get; init; }
[JsonPropertyName("clientCertificateKeyFile")]
public string? ClientCertificateKeyFile { get; init; }
[JsonPropertyName("nodeId")]
public string? NodeId { get; init; }
@@ -74,15 +86,38 @@ public sealed record RemoteAgentOptions
public bool AllowInsecureHttp { get; init; }
public bool IsConfigured => !string.IsNullOrWhiteSpace(AuthAddress)
&& !string.IsNullOrWhiteSpace(Token)
&& (!string.IsNullOrWhiteSpace(Token) || !string.IsNullOrWhiteSpace(TokenFile))
&& !string.IsNullOrWhiteSpace(NodeId);
public string TokenState => string.IsNullOrWhiteSpace(Token) ? "not-configured" : "configured";
public string TokenState => !string.IsNullOrWhiteSpace(TokenFile)
? "file-configured"
: string.IsNullOrWhiteSpace(Token) ? "not-configured" : "configured";
public string GetToken()
{
string? token;
if (string.IsNullOrWhiteSpace(TokenFile))
{
token = Token;
}
else
{
try { token = File.ReadAllText(TokenFile).Trim(); }
catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException)
{
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "tokenFile could not be read.", exception);
}
}
if (string.IsNullOrWhiteSpace(token))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "token or tokenFile must contain a non-empty token.");
return token;
}
public void Validate()
{
if (string.IsNullOrWhiteSpace(AuthAddress) || string.IsNullOrWhiteSpace(Token) || string.IsNullOrWhiteSpace(NodeId))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "authAddress, token and nodeId are required before remote access is enabled.");
if (string.IsNullOrWhiteSpace(AuthAddress) || (!string.IsNullOrWhiteSpace(Token) && !string.IsNullOrWhiteSpace(TokenFile))
|| (!IsConfigured) || string.IsNullOrWhiteSpace(NodeId))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "authAddress, token or tokenFile and nodeId are required before remote access is enabled.");
if (!Uri.TryCreate(AuthAddress, UriKind.Absolute, out var uri) || uri is null
|| uri.AbsolutePath == "/" && uri.Query.Length != 0
|| uri.UserInfo.Length != 0
@@ -92,12 +127,28 @@ public sealed record RemoteAgentOptions
&& (!AllowInsecureHttp || !IsPrivateNetwork(uri.Host)))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "Non-loopback HTTP requires allowInsecureHttp=true and a private-network IP address.");
ValidateIdentifier(NodeId, "nodeId", 200);
if (Token.Any(char.IsWhiteSpace))
var token = GetToken();
if (token.Any(char.IsWhiteSpace))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "token must not contain whitespace.");
if (!string.IsNullOrWhiteSpace(TokenFile) && !File.Exists(TokenFile))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "tokenFile does not exist.");
ValidateOptionalFile(ServerCaFile, "serverCaFile");
ValidateOptionalFile(ClientCertificateFile, "clientCertificateFile");
ValidateOptionalFile(ClientCertificateKeyFile, "clientCertificateKeyFile");
if (!string.IsNullOrWhiteSpace(ClientCertificateKeyFile) && string.IsNullOrWhiteSpace(ClientCertificateFile))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "clientCertificateFile is required with clientCertificateKeyFile.");
if (!string.IsNullOrWhiteSpace(ClientCertificateFile) && string.IsNullOrWhiteSpace(ClientCertificateKeyFile))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, "clientCertificateKeyFile is required for PEM client certificates.");
}
public RemoteAgentOptions Redacted() => this with { Token = string.IsNullOrWhiteSpace(Token) ? null : "<redacted>" };
private static void ValidateOptionalFile(string? path, string name)
{
if (!string.IsNullOrWhiteSpace(path) && !File.Exists(path))
throw new WxAgentException(WxAgentErrorCode.InvalidArgument, $"{name} does not exist.");
}
private static bool IsLoopback(string host) => host.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|| System.Net.IPAddress.TryParse(host.Trim('[', ']'), out var address) && System.Net.IPAddress.IsLoopback(address);