| | | 1 | | using System.Net; |
| | | 2 | | using Microsoft.AspNetCore.HttpOverrides; |
| | | 3 | | |
| | | 4 | | namespace ArturRios.Heimdall.WebApi.Security; |
| | | 5 | | |
| | | 6 | | /// <summary> |
| | | 7 | | /// The reverse proxies allowed to tell the API who its caller is, read once at start-up from |
| | | 8 | | /// <c>HEIMDALL_TRUSTED_PROXIES</c>: a comma-separated list of addresses (<c>172.18.0.2</c>) and |
| | | 9 | | /// CIDR networks (<c>172.18.0.0/16</c>). |
| | | 10 | | /// </summary> |
| | | 11 | | /// <remarks> |
| | | 12 | | /// <para> |
| | | 13 | | /// In production the API runs behind Traefik, so every connection's remote address is |
| | | 14 | | /// Traefik's. The caller's own address arrives in <c>X-Forwarded-For</c> and the original |
| | | 15 | | /// scheme in <c>X-Forwarded-Proto</c>, and ASP.NET Core's forwarded-headers middleware puts |
| | | 16 | | /// them back on the connection. Both the rate limiter's partition key and the client address |
| | | 17 | | /// <c>TraceActivityMiddleware</c> logs read the connection, so without this every caller |
| | | 18 | | /// shares one rate-limit partition and every log line names the proxy. |
| | | 19 | | /// </para> |
| | | 20 | | /// <para> |
| | | 21 | | /// The headers are only believed when the connection comes from a proxy listed here. Any |
| | | 22 | | /// client can send <c>X-Forwarded-For</c>; trusting it from everyone would let a caller pick |
| | | 23 | | /// the address it is rate limited and logged under. For the same reason only one hop is |
| | | 24 | | /// read (the middleware's default <c>ForwardLimit</c> of 1): Traefik appends the address it |
| | | 25 | | /// saw to whatever the client sent, so the rightmost entry is the only one Traefik vouches |
| | | 26 | | /// for. Put a second proxy in front of Traefik and the limit has to rise with it. |
| | | 27 | | /// </para> |
| | | 28 | | /// <para> |
| | | 29 | | /// Unset, nothing is trusted beyond loopback (ASP.NET Core's default), and the headers are |
| | | 30 | | /// not processed at all — the safe reading of a deployment nobody has described. |
| | | 31 | | /// </para> |
| | | 32 | | /// </remarks> |
| | | 33 | | public sealed class TrustedProxyOptions |
| | | 34 | | { |
| | | 35 | | /// <summary>Comma-separated addresses and CIDR networks of the trusted reverse proxies.</summary> |
| | | 36 | | public const string ProxiesVariable = "HEIMDALL_TRUSTED_PROXIES"; |
| | | 37 | | |
| | | 38 | | /// <summary>Trusted proxies given as single addresses.</summary> |
| | 2736 | 39 | | public IReadOnlyList<IPAddress> Proxies { get; private init; } = []; |
| | | 40 | | |
| | | 41 | | /// <summary>Trusted proxies given as CIDR networks.</summary> |
| | 2730 | 42 | | public IReadOnlyList<System.Net.IPNetwork> Networks { get; private init; } = []; |
| | | 43 | | |
| | | 44 | | /// <summary> |
| | | 45 | | /// Entries that were neither an address nor a network, and were ignored. Kept so start-up can |
| | | 46 | | /// say so, instead of silently trusting less than the operator asked for. |
| | | 47 | | /// </summary> |
| | 2724 | 48 | | public IReadOnlyList<string> InvalidEntries { get; private init; } = []; |
| | | 49 | | |
| | | 50 | | /// <summary>Whether any proxy is trusted, and so whether forwarded headers are processed.</summary> |
| | 909 | 51 | | public bool Configured => Proxies.Count > 0 || Networks.Count > 0; |
| | | 52 | | |
| | 899 | 53 | | public static TrustedProxyOptions FromEnvironment() => Parse(Environment.GetEnvironmentVariable(ProxiesVariable)); |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Interprets a <see cref="ProxiesVariable" /> value. Split from <see cref="FromEnvironment" /> |
| | | 57 | | /// so it can be tested without mutating the process environment the functional suite's hosts |
| | | 58 | | /// read. |
| | | 59 | | /// </summary> |
| | | 60 | | public static TrustedProxyOptions Parse(string? value) |
| | | 61 | | { |
| | 910 | 62 | | List<IPAddress> proxies = []; |
| | 910 | 63 | | List<System.Net.IPNetwork> networks = []; |
| | 910 | 64 | | List<string> invalid = []; |
| | | 65 | | |
| | 910 | 66 | | var entries = (value ?? string.Empty) |
| | 910 | 67 | | .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); |
| | | 68 | | |
| | 1844 | 69 | | foreach (var entry in entries) |
| | | 70 | | { |
| | 12 | 71 | | if (entry.Contains('/') && System.Net.IPNetwork.TryParse(entry, out var network)) |
| | | 72 | | { |
| | 3 | 73 | | networks.Add(network); |
| | | 74 | | } |
| | 9 | 75 | | else if (!entry.Contains('/') && IPAddress.TryParse(entry, out var address)) |
| | | 76 | | { |
| | 7 | 77 | | proxies.Add(address); |
| | | 78 | | } |
| | | 79 | | else |
| | | 80 | | { |
| | 2 | 81 | | invalid.Add(entry); |
| | | 82 | | } |
| | | 83 | | } |
| | | 84 | | |
| | 910 | 85 | | return new TrustedProxyOptions { Proxies = proxies, Networks = networks, InvalidEntries = invalid }; |
| | | 86 | | } |
| | | 87 | | |
| | | 88 | | /// <summary> |
| | | 89 | | /// Configures the forwarded-headers middleware to read the client address and scheme from the |
| | | 90 | | /// trusted proxies, and from them only. ASP.NET Core's loopback defaults are replaced rather |
| | | 91 | | /// than added to: the list says exactly who is trusted. |
| | | 92 | | /// </summary> |
| | | 93 | | /// <remarks> |
| | | 94 | | /// <c>X-Forwarded-Host</c> is deliberately not read. Traefik forwards the client's own |
| | | 95 | | /// <c>Host</c> already, and nothing in the API decides anything on the host name — the |
| | | 96 | | /// metrics endpoint is told apart by port precisely because the host is the caller's choice. |
| | | 97 | | /// </remarks> |
| | | 98 | | public void Apply(ForwardedHeadersOptions options) |
| | | 99 | | { |
| | 5 | 100 | | options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; |
| | | 101 | | |
| | 5 | 102 | | options.KnownProxies.Clear(); |
| | 5 | 103 | | options.KnownIPNetworks.Clear(); |
| | | 104 | | |
| | 18 | 105 | | foreach (var proxy in Proxies) |
| | | 106 | | { |
| | 4 | 107 | | options.KnownProxies.Add(proxy); |
| | | 108 | | } |
| | | 109 | | |
| | 12 | 110 | | foreach (var network in Networks) |
| | | 111 | | { |
| | 1 | 112 | | options.KnownIPNetworks.Add(network); |
| | | 113 | | } |
| | 5 | 114 | | } |
| | | 115 | | } |