Caddy Manager CI build / docker (push) Failing after 1m2s
Found while investigating an unrelated Gitea compromise: CaddyManager itself was not involved, but reviewing it turned up three things worth closing. Reading a configuration was the only file operation that did not validate the name. Saving, renaming and deleting all reject `..`, `/` and `\`, so the read path was the one way to leave the configuration directory and pull in any `*.caddy` file on the host. The HTTP API happened to be covered, because GET checks the name against the directory listing first, but the UI calls the service directly and nothing stopped it. Forwarded headers were trusted from any peer. That is correct only while the container port is unreachable except through the proxy; the moment it is published, a caller dictates the scheme, host and client address the app believes in. Loopback and private space cover a proxy on a Docker network or on the host, which is the documented deployment, and ignore everyone else. The README never said that the `X-Api-Key` check guards `/api/*` and nothing else, so the UI - which rewrites Caddyfiles and holds the Docker socket - reads as protected when it is not. It now says so, and warns about the specific shape that bit us: a second hostname added for machine callers whose only extra directive is a `tls` line, which serves the unauthenticated UI to anyone who can resolve it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
4.7 KiB
C#
132 lines
4.7 KiB
C#
using System.Net;
|
|
using CaddyManager.Api;
|
|
using CaddyManager.Components;
|
|
using Microsoft.AspNetCore.Components.Server;
|
|
using Microsoft.AspNetCore.HttpOverrides;
|
|
using Microsoft.OpenApi;
|
|
using MudBlazor.Services;
|
|
using NetCore.AutoRegisterDi;
|
|
using Scalar.AspNetCore;
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Add services to the container.
|
|
builder.Services
|
|
.AddRazorComponents()
|
|
.AddInteractiveServerComponents();
|
|
|
|
// Auto register all the Services, Repositories that we have had within the code base
|
|
builder.Services.RegisterAssemblyPublicNonGenericClasses(System.Reflection.Assembly.GetAssembly(typeof(CaddyManager.Services.Caddy.CaddyService)))
|
|
.Where(t => t.Name.EndsWith("Service"))
|
|
.AsPublicImplementedInterfaces();
|
|
|
|
builder.Services.AddSignalR(e =>
|
|
{
|
|
// Caddyfiles are kilobytes; this is generous headroom for Monaco editor round-trips
|
|
e.MaximumReceiveMessageSize = 512 * 1024;
|
|
e.StreamBufferCapacity = 5;
|
|
});
|
|
|
|
// Keep as little per-circuit state resident as practical for a single-user admin UI
|
|
builder.Services.Configure<CircuitOptions>(o =>
|
|
{
|
|
o.DisconnectedCircuitRetentionPeriod = TimeSpan.FromMinutes(1);
|
|
o.DisconnectedCircuitMaxRetained = 5;
|
|
o.MaxBufferedUnacknowledgedRenderBatches = 3;
|
|
});
|
|
|
|
builder.Services.AddOpenApi(options =>
|
|
{
|
|
// Declare the shared key scheme so the docs page offers an auth box and marks every endpoint as secured
|
|
options.AddDocumentTransformer((document, _, _) =>
|
|
{
|
|
document.Components ??= new OpenApiComponents();
|
|
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
|
|
document.Components.SecuritySchemes[CaddyApi.ApiKeyHeader] = new OpenApiSecurityScheme
|
|
{
|
|
Type = SecuritySchemeType.ApiKey,
|
|
In = ParameterLocation.Header,
|
|
Name = CaddyApi.ApiKeyHeader,
|
|
Description = "Shared key configured through Api:Key (environment variable Api__Key)",
|
|
};
|
|
document.Security ??= [];
|
|
document.Security.Add(new OpenApiSecurityRequirement
|
|
{
|
|
[new OpenApiSecuritySchemeReference(CaddyApi.ApiKeyHeader, document)] = [],
|
|
});
|
|
return Task.CompletedTask;
|
|
});
|
|
});
|
|
|
|
// Caddy terminates TLS and forwards plain HTTP, so without this the app thinks every request is
|
|
// http and the OpenAPI document advertises http:// server URLs, which the browser blocks as mixed
|
|
// content when the docs page itself was served over https
|
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto |
|
|
ForwardedHeaders.XForwardedHost;
|
|
// The proxy is another container on a Docker network, so its address is not known up front.
|
|
// Trusting *any* peer's X-Forwarded-* was too broad: if the port is ever published beyond the
|
|
// proxy, a caller can dictate the scheme, host and client IP the app believes in. Docker
|
|
// networks and loopback are all private, so trusting only private space keeps the intended
|
|
// deployment working while ignoring headers from anywhere else.
|
|
options.KnownIPNetworks.Clear();
|
|
options.KnownProxies.Clear();
|
|
foreach (var network in PrivateProxyNetworks)
|
|
{
|
|
options.KnownIPNetworks.Add(network);
|
|
}
|
|
});
|
|
|
|
builder.Services.AddMudServices(config =>
|
|
{
|
|
config.SnackbarConfiguration.VisibleStateDuration = 4000;
|
|
config.SnackbarConfiguration.HideTransitionDuration = 100;
|
|
config.SnackbarConfiguration.ShowTransitionDuration = 100;
|
|
});
|
|
|
|
var app = builder.Build();
|
|
|
|
// Has to run before anything that reads the scheme, host or client address
|
|
app.UseForwardedHeaders();
|
|
|
|
// Configure the HTTP request pipeline.
|
|
if (!app.Environment.IsDevelopment())
|
|
{
|
|
app.UseExceptionHandler("/Error", createScopeForErrors: true);
|
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
|
app.UseHsts();
|
|
}
|
|
|
|
app.UseHttpsRedirection();
|
|
|
|
|
|
app.UseAntiforgery();
|
|
|
|
app.MapStaticAssets();
|
|
app.MapRazorComponents<App>()
|
|
.AddInteractiveServerRenderMode();
|
|
|
|
app.MapCaddyApi();
|
|
app.MapOpenApi();
|
|
app.MapScalarApiReference();
|
|
|
|
app.Run();
|
|
|
|
/// <summary>
|
|
/// Address space a reverse proxy in front of this app can legitimately come from: loopback, the
|
|
/// RFC 1918 ranges Docker hands out, RFC 4193 unique local addresses and IPv6 loopback
|
|
/// </summary>
|
|
public partial class Program
|
|
{
|
|
internal static readonly System.Net.IPNetwork[] PrivateProxyNetworks =
|
|
[
|
|
new(IPAddress.Parse("127.0.0.0"), 8),
|
|
new(IPAddress.Parse("10.0.0.0"), 8),
|
|
new(IPAddress.Parse("172.16.0.0"), 12),
|
|
new(IPAddress.Parse("192.168.0.0"), 16),
|
|
new(IPAddress.Parse("::1"), 128),
|
|
new(IPAddress.Parse("fc00::"), 7),
|
|
];
|
|
}
|