diff --git a/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs b/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs index 92840be..a3791c6 100644 --- a/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs +++ b/src/PSInfisicalAPI.Tests/CmdletBaseInheritanceTests.cs @@ -84,5 +84,38 @@ namespace PSInfisicalAPI.Tests Assert.Equal("explicit-org", cmdlet.CallResolveOrganizationId(ConnectionWithDefaults(), "explicit-org")); Assert.Empty(logger.VerboseEntries); } + + [Fact] + public void InfisicalConnection_Defaults_TransportFlags_To_False() + { + InfisicalConnection connection = new InfisicalConnection(); + Assert.False(connection.SkipCertificateCheck); + Assert.False(connection.AllowInsecureTransport); + } + + [Fact] + public void ShouldSkipCertificateCheck_Reads_From_Current_Session() + { + InfisicalConnection previous = InfisicalSessionManager.Current; + try + { + TestCmdlet cmdlet = CreateCmdletWith(new RecordingLogger()); + MethodInfo virt = typeof(InfisicalCmdletBase).GetMethod("ShouldSkipCertificateCheck", BindingFlags.NonPublic | BindingFlags.Instance); + + InfisicalSessionManager.SetCurrent(null); + Assert.False((bool)virt.Invoke(cmdlet, null)); + + InfisicalConnection session = ConnectionWithDefaults(); + session.IsConnected = true; + session.SkipCertificateCheck = true; + InfisicalSessionManager.SetCurrent(session); + + Assert.True((bool)virt.Invoke(cmdlet, null)); + } + finally + { + InfisicalSessionManager.SetCurrent(previous); + } + } } } diff --git a/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs b/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs index d199fb9..dd80ace 100644 --- a/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs +++ b/src/PSInfisicalAPI/Cmdlets/ConnectInfisicalCmdlet.cs @@ -62,12 +62,24 @@ namespace PSInfisicalAPI.Cmdlets [Parameter] public SwitchParameter PassThru { get; set; } + [Parameter] + public SwitchParameter SkipCertificateCheck { get; set; } + + [Parameter] + public SwitchParameter AllowInsecureTransport { get; set; } + + protected override bool ShouldSkipCertificateCheck() + { + return SkipCertificateCheck.IsPresent; + } + protected override void ProcessRecord() { try { ResolveMissingParametersFromEnvironment(); ValidateRequiredParameters(); + ValidateTransportSafety(); IInfisicalAuthProvider provider; InfisicalAuthenticationRequest request; @@ -179,7 +191,9 @@ namespace PSInfisicalAPI.Cmdlets ConnectedAtUtc = DateTimeOffset.UtcNow, ExpiresAtUtc = authResult.ExpiresAtUtc, IsConnected = true, - AccessToken = authResult.AccessToken + AccessToken = authResult.AccessToken, + SkipCertificateCheck = SkipCertificateCheck.IsPresent, + AllowInsecureTransport = AllowInsecureTransport.IsPresent }; InfisicalSessionManager.SetCurrent(connection); @@ -195,6 +209,26 @@ namespace PSInfisicalAPI.Cmdlets } } + private void ValidateTransportSafety() + { + bool isHttp = BaseUri != null && string.Equals(BaseUri.Scheme, "http", StringComparison.OrdinalIgnoreCase); + + if (isHttp && !AllowInsecureTransport.IsPresent) + { + throw new InfisicalConfigurationException("BaseUri '" + BaseUri + "' is not HTTPS. Re-run Connect-Infisical with -AllowInsecureTransport to permit plaintext."); + } + + if (SkipCertificateCheck.IsPresent) + { + Logger.Warning(Component, "SkipCertificateCheck is enabled. TLS certificate validation is disabled for this session. Do not use in production."); + } + + if (AllowInsecureTransport.IsPresent && isHttp) + { + Logger.Warning(Component, "AllowInsecureTransport is enabled and BaseUri uses HTTP. Credentials and secrets will traverse the network unencrypted. Do not use in production."); + } + } + private void ResolveMissingParametersFromEnvironment() { bool tokenSet = string.Equals(ParameterSetName, ParameterSetToken, StringComparison.Ordinal); diff --git a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs index 4e35b17..bdc69ce 100644 --- a/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs +++ b/src/PSInfisicalAPI/Cmdlets/InfisicalCmdletBase.cs @@ -31,13 +31,19 @@ namespace PSInfisicalAPI.Cmdlets { if (_httpClient == null) { - _httpClient = new InfisicalHttpClient(Logger); + _httpClient = new InfisicalHttpClient(Logger, 100, ShouldSkipCertificateCheck()); } return _httpClient; } } + protected virtual bool ShouldSkipCertificateCheck() + { + InfisicalConnection current = InfisicalSessionManager.Current; + return current != null && current.SkipCertificateCheck; + } + protected void ThrowTerminatingForException(string component, string operation, Exception exception) { InfisicalErrorDetails details = InfisicalErrorHandler.BuildDetails(component, operation, exception); diff --git a/src/PSInfisicalAPI/Connections/InfisicalConnection.cs b/src/PSInfisicalAPI/Connections/InfisicalConnection.cs index 7e6362d..0eabef2 100644 --- a/src/PSInfisicalAPI/Connections/InfisicalConnection.cs +++ b/src/PSInfisicalAPI/Connections/InfisicalConnection.cs @@ -15,6 +15,8 @@ namespace PSInfisicalAPI.Connections public DateTimeOffset ConnectedAtUtc { get; set; } public DateTimeOffset? ExpiresAtUtc { get; set; } public bool IsConnected { get; set; } + public bool SkipCertificateCheck { get; set; } + public bool AllowInsecureTransport { get; set; } public Dictionary ResolvedEndpointVersions { get; } = new Dictionary(StringComparer.Ordinal); diff --git a/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs b/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs index 55fe767..654c8d9 100644 --- a/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs +++ b/src/PSInfisicalAPI/Http/InfisicalHttpClient.cs @@ -2,6 +2,8 @@ using System; using System.Collections.Generic; using System.IO; using System.Net; +using System.Net.Security; +using System.Reflection; using System.Text; using PSInfisicalAPI.Errors; using PSInfisicalAPI.Logging; @@ -11,13 +13,18 @@ namespace PSInfisicalAPI.Http public sealed class InfisicalHttpClient : IInfisicalHttpClient { private const string Component = "HttpClient"; + private static readonly PropertyInfo PerRequestCertCallbackProperty = + typeof(HttpWebRequest).GetProperty("ServerCertificateValidationCallback"); + private readonly IInfisicalLogger _logger; private readonly int _timeoutSeconds; + private readonly bool _skipCertificateCheck; - public InfisicalHttpClient(IInfisicalLogger logger, int timeoutSeconds = 100) + public InfisicalHttpClient(IInfisicalLogger logger, int timeoutSeconds = 100, bool skipCertificateCheck = false) { _logger = logger ?? NullInfisicalLogger.Instance; _timeoutSeconds = timeoutSeconds; + _skipCertificateCheck = skipCertificateCheck; } public InfisicalHttpResponse Send(InfisicalHttpRequest request) @@ -44,6 +51,11 @@ namespace PSInfisicalAPI.Http webRequest.ReadWriteTimeout = _timeoutSeconds * 1000; webRequest.UseDefaultCredentials = true; + if (_skipCertificateCheck) + { + ApplyInsecureCertificateBypass(webRequest); + } + IWebProxy systemProxy = WebRequest.GetSystemWebProxy(); if (systemProxy != null) { @@ -95,6 +107,20 @@ namespace PSInfisicalAPI.Http } } + private void ApplyInsecureCertificateBypass(HttpWebRequest webRequest) + { + RemoteCertificateValidationCallback callback = (sender, certificate, chain, errors) => true; + + if (PerRequestCertCallbackProperty != null && PerRequestCertCallbackProperty.CanWrite) + { + PerRequestCertCallbackProperty.SetValue(webRequest, callback, null); + return; + } + + _logger.Warning(Component, "Per-request ServerCertificateValidationCallback unavailable on this runtime; falling back to global ServicePointManager override for this process."); + ServicePointManager.ServerCertificateValidationCallback = callback; + } + private static void ApplyHeaders(HttpWebRequest webRequest, IDictionary headers) { if (headers == null)