Ejemplo n.º 1
0
        public IRegistry GetRegistry(RegistryCredentials credentials)
        {
            credentials.Registry = credentials.Registry.ToLowerInvariant();
            if (Registry.DockerHubAliases.Contains(credentials.Registry))
            {
                credentials.Registry = Registry.DockerHub;
            }

            return(new Registry(credentials, Settings));
        }
Ejemplo n.º 2
0
 public ActionResult RegistryCredentials(RegistryCredentials regCreds)
 {
     try
     {
         _protectorService.ProtectAndStore(regCreds.Registry, regCreds);
         return(Ok());
     }
     catch (Exception ex)
     {
         return(BadRequest("Couldn't store credentials: " + ex.Message));
     }
 }
Ejemplo n.º 3
0
        public async Task <IActionResult> Post([FromBody] RegistryCredentials credentials)
        {
            // must specify a registry
            if (string.IsNullOrEmpty(credentials.Registry))
            {
                return(Unauthorized());
            }

            // deny requests for foreign instances, if configured
            if (!string.IsNullOrEmpty(Config.Registry) && credentials.Registry.ToLowerInvariant() != Config.Registry.ToLowerInvariant())
            {
                return(Unauthorized());
            }
            try
            {
                credentials.Registry = RegistryCredentials.DeAliasDockerHub(credentials.Registry);
                var handler = new AuthHandler(cache, Config, loggerFactory.CreateLogger <AuthHandler>());
                await handler.LoginAsync(credentials.Registry, credentials.Username, credentials.Password);

                var json = JsonConvert.SerializeObject(credentials);

                // publicly visible parameters for session validation
                var headers = new Dictionary <string, object>
                {
                    { "iat", DateTimeOffset.UtcNow.ToUnixTimeSeconds() },
                    { "exp", DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.AuthTokenLifetime },
                    { "reg", credentials.Registry }
                };

                var token = new Token
                {
                    Usr = credentials.Username,
                    Pwd = credentials.Password,
                    Reg = credentials.Registry,
                    Iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                    Exp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.AuthTokenLifetime
                };

                var jwe = Jose.JWT.Encode(token, crypto, JweAlgorithm.RSA_OAEP, JweEncryption.A256GCM, extraHeaders: headers);

                return(Ok(new
                {
                    token = jwe
                }));
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "Error authenticating token request.");
                return(Unauthorized());
            }
        }
Ejemplo n.º 4
0
        public static ClaimsIdentity ToClaimsIdentity(this RegistryCredentials credentials)
        {
            var claims = new List <Claim>();

            claims.Add(new Claim(nameof(RegistryCredentials.Registry), credentials.Registry, ClaimValueTypes.String));
            if (!string.IsNullOrEmpty(credentials.Username))
            {
                claims.Add(new Claim(nameof(RegistryCredentials.Username), credentials.Username, ClaimValueTypes.String));
            }
            if (!string.IsNullOrEmpty(credentials.Password))
            {
                claims.Add(new Claim(nameof(RegistryCredentials.Password), credentials.Password, ClaimValueTypes.String));
            }

            var identity = new ClaimsIdentity(claims, "RegistryCredentials");

            return(identity);
        }
Ejemplo n.º 5
0
        public Task <AuthenticateResult> AuthenticateAsync(string authorization)
        {
            try
            {
                var header = AuthenticationHeaderValue.Parse(authorization);
                logger.LogTrace($"Got authorization header: {header}");

                var jweHeader = Jose.JWT.Headers(header.Parameter);
                logger.LogDebug($"JWE Header: {string.Join(", ", jweHeader.Select(p => string.Join(": ", p.Key, p.Value)))}");

                if (!jweHeader.ContainsKey("exp") || (long)jweHeader["exp"] <= DateTimeOffset.UtcNow.ToUnixTimeSeconds())
                {
                    return(Task.FromResult(AuthenticateResult.Fail("{ \"error\": \"The token has expired\" }")));
                }

                var token       = Jose.JWT.Decode <Token>(header.Parameter, crypto);
                var credentials = new RegistryCredentials
                {
                    Password = token.Pwd,
                    Username = token.Usr,
                    Registry = token.Reg
                };
                logger.LogDebug($"Decoded token for {credentials.Registry}");

                if (!string.IsNullOrEmpty(config.Registry) && RegistryCredentials.DeAliasDockerHub(config.Registry.ToLowerInvariant()) != credentials.Registry.ToLowerInvariant())
                {
                    return(Task.FromResult(AuthenticateResult.Fail("{ error: \"The supplied token is for an unsupported registry.\" }")));
                }

                var principal = new ClaimsPrincipal(credentials.ToClaimsIdentity());
                var ticket    = new AuthenticationTicket(principal, "token");

                return(Task.FromResult(AuthenticateResult.Success(ticket)));
            }
            catch (Exception ex)
            {
                logger.LogError(ex, "Token authentication failed.");
                return(Task.FromResult(AuthenticateResult.Fail("{ \"error\": \"The supplied token is invalid.\" }")));
            }
        }
Ejemplo n.º 6
0
        public IActionResult Post([FromBody] RegistryCredentials credentials)
        {
            // must specify a registry
            if (string.IsNullOrEmpty(credentials.Registry))
            {
                return(Unauthorized());
            }

            // deny requests for foreign instances, if configured
            if (!string.IsNullOrEmpty(Config.Catalog?.Registry) && credentials.Registry.ToLowerInvariant() != Config.Catalog.Registry.ToLowerInvariant())
            {
                return(Unauthorized());
            }
            try
            {
                var handler = new AuthHandler(_Cache);
                handler.Login(credentials.Registry, credentials.Username, credentials.Password);
                var json       = JsonConvert.SerializeObject(credentials);
                var cipherText = _Crypto.Encrypt(json);

                return(Ok(new
                {
                    token = Jose.JWT.Encode(new Token
                    {
                        Crd = cipherText,
                        Usr = credentials.Username,
                        Reg = credentials.Registry,
                        Iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
                        Exp = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + Config.Security.TokenLifetime
                    }, _Crypto.ToDotNetRSA(), Jose.JwsAlgorithm.RS256)
                }));
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, "Error authenticating token request.");
                return(Unauthorized());
            }
        }
Ejemplo n.º 7
0
 public PullImageTask(string fqin, string tag, RegistryCredentials regCreds)
 {
     _fqin     = fqin;
     _tag      = tag;
     _regCreds = regCreds;
 }
Ejemplo n.º 8
0
        public override async Task DoRequestAsync(IndexRequest request)
        {
            try
            {
                RegistryCredentials credentials = null;

                // if the request was submitted by a user, it must have auth info included
                if (!string.IsNullOrEmpty(request.Authorization))
                {
                    var authResult = authDecoder.AuthenticateAsync(request.Authorization).Result;
                    if (authResult.Succeeded)
                    {
                        credentials = authResult.Principal.ToRegistryCredentials();
                    }
                }
                // if the request came via an event sink, there is no auth provided, and we need to have a default user configured
                else
                {
                    credentials = config.GetCatalogCredentials() ?? throw new ArgumentException("The indexing request had no included authorization, and no default catalog user is configured.");
                }

                if (credentials == null)
                {
                    logger.LogWarning("Authorization failed for the work item. A token may have expired since it was first submitted.");
                }
                else
                {
                    await authHandler.LoginAsync(credentials);

                    var client = clientFactory.GetClient(authHandler);

                    // if deep indexing is configured, ignore target paths
                    if (config.DeepIndexing)
                    {
                        request.TargetPaths = new string[0];
                    }

                    var imageSet = await client.GetImageSetAsync(request.TargetRepo, request.TargetDigest);

                    if ((imageSet?.Images?.Count() ?? 0) != 1)
                    {
                        throw new Exception($"Couldn't find a valid image for {request.TargetRepo}:{request.TargetDigest}");
                    }
                    var image = imageSet.Images.First();

                    using (var @lock = await cacheFactory.Get <object>().TakeLockAsync($"idx:{image.Digest}", TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5)))
                    {
                        if (!indexStore.IndexExists(image.Digest, request.TargetPaths.ToArray()))
                        {
                            logger.LogInformation($"Starting index for {request.TargetRepo}:{request.TargetDigest}");
                            var indexes = client.GetIndexes(request.TargetRepo, image, request.TargetPaths.ToArray());
                            indexStore.SetIndex(indexes, image.Digest, request.TargetPaths.ToArray());
                            logger.LogInformation($"Completed indexing {indexes.Max(i => i.Depth)} layer(s) from {request.TargetRepo}:{request.TargetDigest} {(request.TargetPaths.Count() == 0 ? "" : $"({string.Join(", ", request.TargetPaths)})")}");
                        }
                        else
                        {
                            logger.LogInformation($"Index already exists for {request.TargetRepo}:{request.TargetDigest}");
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"Processing failed for work item\n {Newtonsoft.Json.JsonConvert.SerializeObject(request)}");
            }
        }
Ejemplo n.º 9
0
        /// <summary>
        /// Submits all layers of an image to Clair for scanning. If a layer has been scanned previously, it will be skipped.
        /// Repository info is necessary to download blobs for scanning, but once scanned from any repository blobs do not need to
        /// be rescanned/analyzed. Calling GetScan(hard: true) will always return the most current available vulnerability analysis.
        /// </summary>
        /// <param name="registry"></param>
        /// <param name="repository"></param>
        /// <param name="image"></param>
        public void RequestScan(string repository, Image image, string host, string authorization)
        {
            var cache    = cacheFactory.Get <Result>();
            var lockTime = new TimeSpan(0, 5, 0);

            // if this image is already in cache, skip it entirely
            if (!cache.ExistsAsync(GetKey(image)).Result)
            {
                bool   layerErrors = false;
                string lastError   = string.Empty;
                using (var scanlock = cache.TakeLockAsync($"scan:{image.Digest}", lockTime, lockTime).Result)
                {
                    Layer previousLayer = null;
                    foreach (var layer in image.Layers.Reverse())
                    {
                        if (!CheckLayerScanned(layer))
                        {
                            var uri = new Uri(RegistryCredentials.HostToEndpoint(host, $"{repository}/blobs/{layer.Digest}"));

                            var request = new ClairLayerRequest
                            {
                                Layer = new ClairLayerRequest.LayerRequest
                                {
                                    Name       = layer.Digest,
                                    ParentName = previousLayer?.Digest,
                                    Path       = uri.ToString(),
                                    Headers    = string.IsNullOrEmpty(authorization) ? null : new ClairLayerRequest.LayerRequest.LayerRequestHeaders {
                                        Authorization = authorization
                                    }
                                }
                            };

                            try
                            {
                                var tokenSource = new CancellationTokenSource();
                                tokenSource.CancelAfter(60000);
                                clair.SubmitLayer(request, tokenSource.Token).Wait();
                            }
                            catch (AggregateException ex)
                            {
                                if (ex.InnerException is ApiException && ((ApiException)ex.InnerException).StatusCode == (System.Net.HttpStatusCode) 422)
                                {
                                    // at least one layer had issues, which may or may not have invalidated the scan
                                    // this can be transient, it can be a false error, or it can genuinely mean the image is currently unscannable
                                    // https://github.com/coreos/clair/issues/543
                                    layerErrors = true;
                                    var errorContent = (ex.InnerException as ApiException).Content;
                                    try
                                    {
                                        var json = JObject.Parse(errorContent);
                                        lastError = (string)json["Error"]["Message"];
                                    }
                                    catch { logger.LogError(ex, $"Could not parse Clair error response '{errorContent}'"); }
                                    continue;
                                }
                                else
                                {
                                    throw;
                                }
                            }
                        }
                        previousLayer = layer;
                    }

                    // if any layers failed above, check that we can get a valid result, and if not set an error entry so we can avoid further attempts for now
                    if (layerErrors && !CheckLayerScanned(image.Layers.First()))
                    {
                        cache.SetAsync(GetKey(image), new Result
                        {
                            Status  = RequestStatus.Failed,
                            Message = lastError ?? "At least one layer of the image could not be scanned."
                        }).Wait();
                    }
                }
            }
        }
Ejemplo n.º 10
0
        public override async Task DoRequestAsync(ScanRequest request)
        {
            try
            {
                RegistryCredentials credentials = null;

                // if the request was submitted by a user, it must have auth info included
                if (!string.IsNullOrEmpty(request.Authorization))
                {
                    var authResult = authDecoder.AuthenticateAsync(request.Authorization).Result;
                    if (authResult.Succeeded)
                    {
                        credentials = authResult.Principal.ToRegistryCredentials();
                    }
                }
                // if the request came via an event sink, there is no auth provided, and we need to have a default user configured
                else
                {
                    credentials = config.GetCatalogCredentials() ?? throw new ArgumentException("The indexing request had no included authorization, and no default catalog user is configured.");
                }

                if (credentials == null)
                {
                    logger.LogError("Authorization failed for the work item. A token may have expired since it was first submitted.");
                }
                else
                {
                    await authHandler.LoginAsync(credentials);

                    var scope = authHandler.RepoPullScope(request.TargetRepo);
                    if (await authHandler.AuthorizeAsync(scope))
                    {
                        var proxyAuth = authHandler.TokensRequired ? $"Bearer {(await authHandler.GetAuthorizationAsync(scope)).Parameter}" : string.Empty;
                        var client    = clientFactory.GetClient(authHandler);

                        var imageSet = await client.GetImageSetAsync(request.TargetRepo, request.TargetDigest);

                        if ((imageSet?.Images?.Count() ?? 0) != 1)
                        {
                            throw new Exception($"Couldn't find a valid image for {request.TargetRepo}:{request.TargetDigest}");
                        }

                        var scanResult = scanner.GetScan(imageSet.Images.First());
                        if (scanResult == null)
                        {
                            if (request.Submitted)
                            {
                                // we've already submitted this one to the scanner, just sleep on it for a few seconds
                                Thread.Sleep(2000);
                            }
                            else
                            {
                                var host = authHandler.GetRegistryHost();
                                scanner.RequestScan(request.TargetRepo, imageSet.Images.First(), host, proxyAuth);
                                logger.LogInformation($"Submitted {request.TargetRepo}:{request.TargetDigest} to {scanner.GetType().Name} for analysis.");
                                request.Submitted = true;
                            }
                            queue.Push(request);
                        }
                        else
                        {
                            logger.LogInformation($"Got latest {scanner.GetType().Name} scan for {request.TargetRepo}:{request.TargetDigest}");
                        }
                    }
                    else
                    {
                        logger.LogError($"Failed to get pull authorization for {request.TargetRepo}");
                    }
                }
            }
            catch (Exception ex)
            {
                logger.LogError(ex, $"Processing failed for work item\n {Newtonsoft.Json.JsonConvert.SerializeObject(request)}");
            }
        }
Ejemplo n.º 11
0
        /// <inheritdoc/>
        public void Run(ModuleContext context)
        {
            var    hive = HiveHelper.Hive;
            string hostname;

            if (!context.ValidateArguments(context.Arguments, validModuleArgs))
            {
                context.Failed = true;
                return;
            }

            // Obtain common arguments.

            context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [state]");

            if (!context.Arguments.TryGetValue <string>("state", out var state))
            {
                state = "present";
            }

            state = state.ToLowerInvariant();

            if (context.HasErrors)
            {
                return;
            }

            var manager      = hive.GetReachableManager();
            var sbErrorNodes = new StringBuilder();

            // Determine whether the registry service is already deployed and
            // also retrieve the registry credentials from Vault if present.
            // Note that the current registry hostname will be persisted to
            // Consul at [neon/service/neon-registry/hostname] when a registry
            // is deployed.

            context.WriteLine(AnsibleVerbosity.Trace, $"Inspecting the [neon-registry] service.");

            var currentService = hive.Docker.InspectService("neon-registry");

            context.WriteLine(AnsibleVerbosity.Trace, $"Getting current registry hostname from Consul.");

            var currentHostname = hive.Registry.GetLocalHostname();
            var currentSecret   = hive.Registry.GetLocalSecret();
            var currentImage    = currentService?.Spec.TaskTemplate.ContainerSpec.ImageWithoutSHA;

            var currentCredentials =        // Set blank properties for the change detection below.
                                     new RegistryCredentials()
            {
                Registry = string.Empty,
                Username = string.Empty,
                Password = string.Empty
            };

            if (!string.IsNullOrEmpty(currentHostname))
            {
                context.WriteLine(AnsibleVerbosity.Trace, $"Reading existing registry credentials for [{currentHostname}].");

                currentCredentials = hive.Registry.GetCredentials(currentHostname);

                if (currentCredentials != null)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] exist.");
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"Registry credentials for [{currentHostname}] do not exist.");
                }
            }

            // Obtain the current registry TLS certificate (if any).

            var currentCertificate = hive.Certificate.Get("neon-registry");

            // Perform the operation.

            switch (state)
            {
            case "absent":

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]");

                if (!context.Arguments.TryGetValue <string>("hostname", out hostname))
                {
                    throw new ArgumentException($"[hostname] module argument is required.");
                }

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "[neon-registry] is not currently deployed.");
                }

                if (context.CheckMode)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be removed when CHECK-MODE is disabled.");
                    return;
                }

                if (currentService == null)
                {
                    return;     // Nothing to do
                }

                context.Changed = true;

                // Logout of the registry.

                if (currentCredentials != null)
                {
                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                    hive.Registry.Logout(currentHostname);
                }

                // Delete the [neon-registry] service and volume.  Note that
                // the volume should exist on all of the manager nodes.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] service.");
                manager.DockerCommand(RunOptions.None, "docker", "service", "rm", "neon-registry");

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] volumes.");

                var volumeRemoveActions = new List <Action>();
                var volumeRetryPolicy   = new LinearRetryPolicy(typeof(TransientException), maxAttempts: 10, retryInterval: TimeSpan.FromSeconds(2));

                foreach (var node in hive.Managers)
                {
                    volumeRemoveActions.Add(
                        () =>
                    {
                        // $hack(jeff.lill):
                        //
                        // Docker service removal appears to be synchronous but the removal of the
                        // actual service task containers is not.  We're going to detect this and
                        // throw a [TransientException] and then retry.

                        using (var clonedNode = node.Clone())
                        {
                            lock (context)
                            {
                                context.WriteLine(AnsibleVerbosity.Trace, $"Removing [neon-registry] volume on [{clonedNode.Name}].");
                            }

                            volumeRetryPolicy.InvokeAsync(
                                async() =>
                            {
                                var response = clonedNode.DockerCommand(RunOptions.None, "docker", "volume", "rm", "neon-registry");

                                if (response.ExitCode != 0)
                                {
                                    var message = $"Error removing [neon-registry] volume from [{clonedNode.Name}: {response.ErrorText}";

                                    lock (syncLock)
                                    {
                                        context.WriteLine(AnsibleVerbosity.Info, message);
                                    }

                                    if (response.AllText.Contains("volume is in use"))
                                    {
                                        throw new TransientException(message);
                                    }
                                }
                                else
                                {
                                    lock (context)
                                    {
                                        context.WriteLine(AnsibleVerbosity.Trace, $"Removed [neon-registry] volume on [{clonedNode.Name}].");
                                    }
                                }

                                await Task.Delay(0);
                            }).Wait();
                        }
                    });
                }

                NeonHelper.WaitForParallel(volumeRemoveActions);

                // Remove the traffic manager rule and certificate.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager rule.");
                hive.PublicTraffic.RemoveRule("neon-registry");
                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] traffic manager certificate.");
                hive.Certificate.Remove("neon-registry");

                // Remove any related Consul state.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [neon-registry] Consul [hostname] and [secret].");
                hive.Registry.SetLocalHostname(null);
                hive.Registry.SetLocalSecret(null);

                // Logout the hive from the registry.

                context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                hive.Registry.Logout(currentHostname);

                // Remove the hive DNS host entry.

                context.WriteLine(AnsibleVerbosity.Trace, $"Removing the [{currentHostname}] registry DNS hosts entry.");
                hive.Dns.Remove(hostname);
                break;

            case "present":

                if (!hive.Definition.HiveFS.Enabled)
                {
                    context.WriteErrorLine("The local registry service requires hive CephFS.");
                    return;
                }

                // Parse the [hostname], [certificate], [username] and [password] arguments.

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [hostname]");

                if (!context.Arguments.TryGetValue <string>("hostname", out hostname))
                {
                    throw new ArgumentException($"[hostname] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [certificate]");

                if (!context.Arguments.TryGetValue <string>("certificate", out var certificatePem))
                {
                    throw new ArgumentException($"[certificate] module argument is required.");
                }

                if (!TlsCertificate.TryParse(certificatePem, out var certificate))
                {
                    throw new ArgumentException($"[certificate] is not a valid certificate.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [username]");

                if (!context.Arguments.TryGetValue <string>("username", out var username))
                {
                    throw new ArgumentException($"[username] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [password]");

                if (!context.Arguments.TryGetValue <string>("password", out var password))
                {
                    throw new ArgumentException($"[password] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [secret]");

                if (!context.Arguments.TryGetValue <string>("secret", out var secret) || string.IsNullOrEmpty(secret))
                {
                    throw new ArgumentException($"[secret] module argument is required.");
                }

                context.WriteLine(AnsibleVerbosity.Trace, $"Parsing [image]");

                if (!context.Arguments.TryGetValue <string>("image", out var image))
                {
                    image = HiveConst.NeonProdRegistry + "/neon-registry:latest";
                }

                // Detect service changes.

                var hostnameChanged    = hostname != currentCredentials?.Registry;
                var usernameChanged    = username != currentCredentials?.Username;
                var passwordChanged    = password != currentCredentials?.Password;
                var secretChanged      = secret != currentSecret;
                var imageChanged       = image != currentImage;
                var certificateChanged = certificate?.CombinedPemNormalized != currentCertificate?.CombinedPemNormalized;
                var updateRequired     = hostnameChanged ||
                                         usernameChanged ||
                                         passwordChanged ||
                                         secretChanged ||
                                         imageChanged ||
                                         certificateChanged;

                if (hostnameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[hostname] changed from [{currentCredentials?.Registry}] --> [{hostname}]");
                }

                if (usernameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[username] changed from [{currentCredentials?.Username}] --> [{username}]");
                }

                if (usernameChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[password] changed from [{currentCredentials?.Password}] --> [**REDACTED**]");
                }

                if (secretChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[secret] changed from [{currentSecret}] --> [**REDACTED**]");
                }

                if (imageChanged)
                {
                    context.WriteLine(AnsibleVerbosity.Info, $"[image] changed from [{currentImage}] --> [{image}]");
                }

                if (certificateChanged)
                {
                    var currentCertRedacted = currentCertificate != null ? "**REDACTED**" : "**NONE**";

                    context.WriteLine(AnsibleVerbosity.Info, $"[certificate] changed from [{currentCertRedacted}] --> [**REDACTED**]");
                }

                // Handle CHECK-MODE.

                if (context.CheckMode)
                {
                    if (currentService == null)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"Local registry will be deployed when CHECK-MODE is disabled.");
                        return;
                    }

                    if (updateRequired)
                    {
                        context.WriteLine(AnsibleVerbosity.Important, $"One or more of the arguments have changed so the registry will be updated when CHECK-MODE is disabled.");
                        return;
                    }

                    return;
                }

                // Create the hive DNS host entry we'll use to redirect traffic targeting the registry
                // hostname to the hive managers.  We need to do this because registry IP addresses
                // are typically public, typically targeting the external firewall or load balancer
                // interface.
                //
                // The problem is that hive nodes will generally be unable to connect to the
                // local managers through the firewall/load balancer because most network routers
                // block network traffic that originates from inside the hive, then leaves
                // to hit the external router interface with the expectation of being routed
                // back inside.  I believe this is an anti-spoofing security measure.

                var dnsRedirect = GetRegistryDnsEntry(hostname);

                // Perform the operation.

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service needs to be created.");
                    context.Changed = true;

                    // The registry service isn't running, so we'll do a full deployment.

                    context.WriteLine(AnsibleVerbosity.Trace, $"Setting certificate.");
                    hive.Certificate.Set("neon-registry", certificate);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Updating Consul settings.");
                    hive.Registry.SetLocalHostname(hostname);
                    hive.Registry.SetLocalSecret(secret);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Adding hive DNS host entry for [{hostname}].");
                    hive.Dns.Set(dnsRedirect, waitUntilPropagated: true);

                    context.WriteLine(AnsibleVerbosity.Trace, $"Writing traffic manager rule.");
                    hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname));

                    context.WriteLine(AnsibleVerbosity.Trace, $"Creating the [neon-registry] service.");

                    var createResponse = manager.DockerCommand(RunOptions.None,
                                                               "docker service create",
                                                               "--name", "neon-registry",
                                                               "--mode", "global",
                                                               "--constraint", "node.role==manager",
                                                               "--env", $"USERNAME={username}",
                                                               "--env", $"PASSWORD={password}",
                                                               "--env", $"SECRET={secret}",
                                                               "--env", $"LOG_LEVEL=info",
                                                               "--env", $"READ_ONLY=false",
                                                               "--mount", "type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry",
                                                               "--network", "neon-public",
                                                               "--restart-delay", "10s",
                                                               image);

                    if (createResponse.ExitCode != 0)
                    {
                        context.WriteErrorLine($"[neon-registry] service create failed: {createResponse.ErrorText}");
                        return;
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Service created.");
                    context.WriteLine(AnsibleVerbosity.Trace, $"Wait for [neon-registry] service to stabilize (30s).");
                    Thread.Sleep(TimeSpan.FromSeconds(30));
                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry.");
                    hive.Registry.Login(hostname, username, password);
                }
                else if (updateRequired)
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is required.");
                    context.Changed = true;

                    // Update the service and related settings as required.

                    if (certificateChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating certificate.");
                        hive.Certificate.Set("neon-registry", certificate);
                    }

                    if (hostnameChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating traffic manager rule.");
                        hive.PublicTraffic.SetRule(GetRegistryTrafficManagerRule(hostname));

                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating hive DNS host entry for [{hostname}] (60 seconds).");
                        hive.Dns.Set(dnsRedirect, waitUntilPropagated: true);

                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive hostname [{hostname}].");
                        hive.Registry.SetLocalHostname(hostname);

                        if (!string.IsNullOrEmpty(currentHostname))
                        {
                            context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive out of the [{currentHostname}] registry.");
                            hive.Registry.Logout(currentHostname);
                        }
                    }

                    if (secretChanged)
                    {
                        context.WriteLine(AnsibleVerbosity.Trace, $"Updating local hive secret.");
                        hive.Registry.SetLocalSecret(secret);
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Updating service.");

                    var updateResponse = manager.DockerCommand(RunOptions.None,
                                                               "docker service update",
                                                               "--env-add", $"USERNAME={username}",
                                                               "--env-add", $"PASSWORD={password}",
                                                               "--env-add", $"SECRET={secret}",
                                                               "--env-add", $"LOG_LEVEL=info",
                                                               "--env-add", $"READ_ONLY=false",
                                                               "--image", image,
                                                               "neon-registry");

                    if (updateResponse.ExitCode != 0)
                    {
                        context.WriteErrorLine($"[neon-registry] service update failed: {updateResponse.ErrorText}");
                        return;
                    }

                    context.WriteLine(AnsibleVerbosity.Trace, $"Service updated.");

                    context.WriteLine(AnsibleVerbosity.Trace, $"Logging the hive into the [{hostname}] registry.");
                    hive.Registry.Login(hostname, username, password);
                }
                else
                {
                    context.WriteLine(AnsibleVerbosity.Important, $"[neon-registry] service update is not required but we're logging all nodes into [{hostname}] to ensure hive consistency.");
                    hive.Registry.Login(hostname, username, password);

                    context.Changed = false;
                }
                break;

            case "prune":

                if (currentService == null)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "Registry service is not running.");
                    return;
                }

                if (context.CheckMode)
                {
                    context.WriteLine(AnsibleVerbosity.Important, "Registry will be pruned when CHECK-MODE is disabled.");
                    return;
                }

                context.Changed = true;     // Always set this to TRUE for prune.

                // We're going to upload a script to one of the managers that handles
                // putting the [neon-registry] service into READ-ONLY mode, running
                // the garbage collection container and then restoring [neon-registry]
                // to READ/WRITE mode.
                //
                // The nice thing about this is that the operation will continue to
                // completion on the manager node even if we lose the SSH connection.

                var updateScript =
                    $@"#!/bin/bash
# Update [neon-registry] to READ-ONLY mode:

docker service update --env-rm READ_ONLY --env-add READ_ONLY=true neon-registry

# Prune the registry:

docker run \
   --name neon-registry-prune \
   --restart-condition=none \
   --mount type=volume,src=neon-registry,volume-driver=neon,dst=/var/lib/neon-registry \
   {HiveConst.NeonProdRegistry}/neon-registry garbage-collect

# Restore [neon-registry] to READ/WRITE mode:

docker service update --env-rm READ_ONLY --env-add READ_ONLY=false neon-registry
";
                var bundle = new CommandBundle("./collect.sh");

                bundle.AddFile("collect.sh", updateScript, isExecutable: true);

                context.WriteLine(AnsibleVerbosity.Info, "Registry prune started.");

                var pruneResponse = manager.SudoCommand(bundle, RunOptions.None);

                if (pruneResponse.ExitCode != 0)
                {
                    context.WriteErrorLine($"The prune operation failed.  The registry may be running in READ-ONLY mode: {pruneResponse.ErrorText}");
                    return;
                }

                context.WriteLine(AnsibleVerbosity.Info, "Registry prune completed.");
                break;

            default:

                throw new ArgumentException($"[state={state}] is not one of the valid choices: [present], [absent], or [prune].");
            }
        }
Ejemplo n.º 12
0
 public string GetRegistryEndpoint(bool ignoreAliases = false) =>
 RegistryCredentials.HostToEndpoint(GetRegistryHost(ignoreAliases));
Ejemplo n.º 13
0
        /// <summary>
        /// Initializes the set of authorizations for this auth handler, and validates credentials with the registry service.
        /// </summary>
        public async Task LoginAsync(RegistryCredentials credentials)
        {
            AnonymousMode       = false;
            RegistryCredentials = credentials;

            var host     = GetRegistryHost(config.IgnoreInternalAlias);
            var endpoint = GetRegistryEndpoint(config.IgnoreInternalAlias);

            var key = GetKey(null, granted: true);

            logger.LogDebug($"Logging in to {endpoint} ({host})");

            if (await authCache.ExistsAsync(key))
            {
                logger.LogDebug($"Found cached credentials ({key})");
                var authorization = await authCache.GetAsync(key);

                Service       = authorization.Service;
                Realm         = authorization.Realm;
                AnonymousMode = authorization.Anonymous;
                DockerHub     = authorization.DockerHub;
            }
            else
            {
                using (var client = new HttpClient())
                {
                    AnonymousMode = (string.IsNullOrEmpty(username) && string.IsNullOrEmpty(password));

                    if (AnonymousMode)
                    {
                        logger.LogDebug("Client is in anon mode.");
                    }

                    // if this is a docker hub client, we can't really validate until we have a real resource to fetch,
                    // but we can set some known values
                    if (host == RegistryCredentials.DockerHub)
                    {
                        DockerHub = true;
                        Realm     = RegistryCredentials.DockerRealm;
                        Service   = RegistryCredentials.DockerService;
                        return;
                    }

                    // first try an anonymous get for the registry catalog - if that succeeds but we're not in anonymous mode, throw
                    var preLogin = await client.GetAsync(endpoint + "/");

                    if (preLogin.IsSuccessStatusCode)
                    {
                        if (!AnonymousMode)
                        {
                            throw new AuthenticationException("A username or password was specified, but the registry server does not support authentication.");
                        }
                        else
                        {
                            // we're anon, and an anon catalog fetch worked, so we should be golden
                            return;
                        }
                    }
                    else if (preLogin.StatusCode == HttpStatusCode.Unauthorized)
                    {
                        var challenge = ParseWwwAuthenticate(preLogin.Headers.WwwAuthenticate.First());
                        Service = challenge.service;
                        Realm   = challenge.realm;

                        if (await UpdateAuthorizationAsync(null))
                        {
                            client.DefaultRequestHeaders.Authorization = await GetAuthorizationAsync(null);

                            var confirmation = client.GetAsync(endpoint + "/").Result;

                            if (!confirmation.IsSuccessStatusCode)
                            {
                                throw new AuthenticationException($"Authentication succeded against the realm '{Realm}', but the registry returned status code '{confirmation.StatusCode}'");
                            }
                            else
                            {
                                await authCache.SetAsync(key, new Authorization { JWT = (await GetAuthorizationAsync(null)).Parameter, Realm = Realm, Service = Service, Anonymous = AnonymousMode }, AuthTtl);
                            }
                        }
                        else
                        {
                            throw new AuthenticationException($"Authentication failed.");
                        }
                    }
                    else
                    {
                        throw new AuthenticationException($"The registry server returned an unexpected result code: {preLogin.StatusCode}");
                    }
                }
            }
        }