diff options
43 files changed, 642 insertions, 219 deletions
diff --git a/client/go/script-utils/main.go b/client/go/script-utils/main.go index 713a208a319..9d35cb46e1c 100644 --- a/client/go/script-utils/main.go +++ b/client/go/script-utils/main.go @@ -50,6 +50,13 @@ func main() { } else { os.Exit(1) } + case "detect-hostname": + myName, err := vespa.FindOurHostname() + fmt.Println(myName) + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } case "vespa-deploy": cobra := deploy.NewDeployCmd() cobra.Execute() @@ -70,7 +77,7 @@ func main() { os.Exit(startcbinary.Run(os.Args)) } fmt.Fprintf(os.Stderr, "unknown action '%s'\n", action) - fmt.Fprintln(os.Stderr, "actions: export-env, ipv6-only, security-env") + fmt.Fprintln(os.Stderr, "actions: export-env, ipv6-only, security-env, detect-hostname") fmt.Fprintln(os.Stderr, "(also: vespa-deploy, vespa-logfmt)") } } diff --git a/client/go/vespa/prestart.go b/client/go/vespa/prestart.go index eb4a598ef37..c102e1ed41b 100644 --- a/client/go/vespa/prestart.go +++ b/client/go/vespa/prestart.go @@ -31,7 +31,6 @@ func RunPreStart() error { } fixSpec.FixDir("logs") fixSpec.FixDir("logs/vespa") - fixSpec.FixDir("logs/vespa/access") fixSpec.FixDir("logs/vespa/configserver") fixSpec.FixDir("logs/vespa/search") fixSpec.FixDir("var/tmp") @@ -54,9 +53,10 @@ func RunPreStart() error { fixSpec.FixDir("var/vespa/bundlecache") fixSpec.FixDir("var/vespa/bundlecache/configserver") fixSpec.FixDir("var/vespa/cache/config") + // fix wrong ownerships within directories: var fixer fs.WalkDirFunc = func(path string, d fs.DirEntry, err error) error { if err != nil { - panic(err) + util.JustExitWith(err) } if d.IsDir() { fixSpec.FixDir(path) @@ -68,5 +68,20 @@ func RunPreStart() error { fileSystem := os.DirFS(vespaHome) fs.WalkDir(fileSystem, "logs/vespa", fixer) fs.WalkDir(fileSystem, "var/db/vespa", fixer) + // we used to have (Vespa 7) a directory "qrs", with "access" a symlink to that. + // now we want it the other way around, try to make it so: + const lva string = "logs/vespa/access" + const lvq string = "logs/vespa/qrs" + info, err := os.Lstat(lva) + if err == nil && (info.Mode()&os.ModeSymlink) != 0 { + err = os.Remove(lva) + if err != nil { + return err + } + } + fixSpec.FixDir(lva) + // best-effort fixup, not trying too hard to get backwards compatibility: + _ = os.Remove(lvq) + _ = os.Symlink("access", lvq) return nil } diff --git a/container-core/src/main/java/com/yahoo/jdisc/http/filter/util/FilterTestUtils.java b/container-core/src/main/java/com/yahoo/jdisc/http/filter/util/FilterTestUtils.java new file mode 100644 index 00000000000..147c13c8e3a --- /dev/null +++ b/container-core/src/main/java/com/yahoo/jdisc/http/filter/util/FilterTestUtils.java @@ -0,0 +1,103 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.jdisc.http.filter.util; + +import com.yahoo.jdisc.Container; +import com.yahoo.jdisc.References; +import com.yahoo.jdisc.Request; +import com.yahoo.jdisc.ResourceReference; +import com.yahoo.jdisc.handler.ContentChannel; +import com.yahoo.jdisc.handler.RequestHandler; +import com.yahoo.jdisc.handler.ResponseHandler; +import com.yahoo.jdisc.http.Cookie; +import com.yahoo.jdisc.http.HttpRequest; +import com.yahoo.jdisc.http.HttpRequest.Method; +import com.yahoo.jdisc.http.HttpRequest.Version; +import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.SecurityRequestFilter; +import com.yahoo.jdisc.http.filter.SecurityResponseFilter; +import com.yahoo.jdisc.http.server.jetty.RequestUtils; +import com.yahoo.jdisc.service.CurrentContainer; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.security.Principal; +import java.security.cert.X509Certificate; +import java.time.Clock; +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +import static com.yahoo.jdisc.http.HttpRequest.Version.HTTP_1_1; + +/** + * Test helper for {@link SecurityRequestFilter}/{@link SecurityResponseFilter}. + * + * @author bjorncs + */ +public class FilterTestUtils { + + private FilterTestUtils() {} + + public static RequestBuilder newRequestBuilder() { return new RequestBuilder(); } + + public static class RequestBuilder { + + private URI uri = URI.create("https://localhost:443/"); + private Method method = Method.GET; + private Clock clock = Clock.systemUTC(); + private Principal principal; + private final Map<String, Object> attributes = new TreeMap<>(); + private List<X509Certificate> certificates = List.of(); + private final Map<String, String> headers = new TreeMap<>(); + private Version version = HTTP_1_1; + private SocketAddress remoteAddress; + private final List<Cookie> cookies = new ArrayList<>(); + + private RequestBuilder() {} + + public RequestBuilder withUri(String uri) { return withUri(URI.create(uri)); } + public RequestBuilder withUri(URI uri) { this.uri = uri; return this; } + public RequestBuilder withMethod(String method) { return withMethod(Method.valueOf(method)); } + public RequestBuilder withMethod(Method method) { this.method = method; return this; } + public RequestBuilder withClock(Clock clock) { this.clock = clock; return this; } + public RequestBuilder withPrincipal(Principal principal) { this.principal = principal; return this; } + public RequestBuilder withAttribute(String name, Object value) { attributes.put(name, value); return this; } + public RequestBuilder withClientCertificate(X509Certificate cert) { return withClientCertificate(List.of(cert)); } + public RequestBuilder withClientCertificate(List<X509Certificate> certs) { certificates = List.copyOf(certs); return this; } + public RequestBuilder withHeader(String name, String value) { headers.put(name, value); return this; } + public RequestBuilder withHttpVersion(Version version) { this.version = version; return this; } + public RequestBuilder withRemoteAddress(String host, int port) { return withRemoteAddress(new InetSocketAddress(host, port)); } + public RequestBuilder withRemoteAddress(SocketAddress address) { this.remoteAddress = address; return this; } + public RequestBuilder withCookie(String cookie) { cookies.addAll(Cookie.fromCookieHeader(cookie)); return this; } + public RequestBuilder withCookie(Cookie cookie) { cookies.add(cookie); return this; } + + public DiscFilterRequest build() { + var httpReq = HttpRequest.newServerRequest( + new DummyContainer(clock), uri, method, version, remoteAddress, clock.millis()); + var filterReq = new DiscFilterRequest(httpReq); + filterReq.setUserPrincipal(principal); + attributes.forEach(filterReq::setAttribute); + filterReq.setAttribute(RequestUtils.JDISC_REQUEST_X509CERT, certificates.toArray(X509Certificate[]::new)); + headers.forEach(filterReq::addHeader); + filterReq.setCookies(cookies); + return filterReq; + } + } + + private record DummyContainer(Clock clock) implements CurrentContainer, Container, RequestHandler { + @Override public RequestHandler resolveHandler(Request request) { return this; } + @Override public <T> T getInstance(Class<T> type) { throw new UnsupportedOperationException(); } + @Override public void release() {} + @Override public long currentTimeMillis() { return clock.millis(); } + @Override public ContentChannel handleRequest(Request request, ResponseHandler handler) { throw new UnsupportedOperationException(); } + @Override public void handleTimeout(Request request, ResponseHandler handler) { throw new UnsupportedOperationException(); } + @Override public Container newReference(URI uri, Object context) { return this; } + @Override public Container newReference(URI uri) { return this; } + @Override public ResourceReference refer(Object context) { return References.NOOP_REFERENCE; } + @Override public ResourceReference refer() { return References.NOOP_REFERENCE; } + @Override public Instant currentTime() { return clock.instant(); } + } +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java index bf16913d05a..e49e9c7998e 100644 --- a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/ServiceRegistry.java @@ -7,6 +7,7 @@ import com.yahoo.vespa.hosted.controller.api.identifiers.ControllerVersion; import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.artifact.ArtifactRegistry; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService; +import com.yahoo.vespa.hosted.controller.api.integration.aws.EnclaveAccessService; import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; @@ -86,6 +87,8 @@ public interface ServiceRegistry { ResourceTagger resourceTagger(); + EnclaveAccessService enclaveAccessService(); + RoleService roleService(); SystemMonitor systemMonitor(); diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java new file mode 100644 index 00000000000..52e8ba5adf8 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/EnclaveAccessService.java @@ -0,0 +1,15 @@ +package com.yahoo.vespa.hosted.controller.api.integration.aws; + +import com.yahoo.config.provision.CloudAccount; + +import java.util.Set; + +/** + * @author jonmv + */ +public interface EnclaveAccessService { + + /** Ensures the given enclave accounts have access to resources they require to function. */ + void allowAccessFor(Set<CloudAccount> accounts); + +} diff --git a/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java new file mode 100644 index 00000000000..81163404007 --- /dev/null +++ b/controller-api/src/main/java/com/yahoo/vespa/hosted/controller/api/integration/aws/MockEnclaveAccessService.java @@ -0,0 +1,22 @@ +package com.yahoo.vespa.hosted.controller.api.integration.aws; + +import com.yahoo.config.provision.CloudAccount; + +import java.util.Set; +import java.util.TreeSet; + +/** + * @author jonmv + */ +public class MockEnclaveAccessService implements EnclaveAccessService { + + private volatile Set<CloudAccount> currentAccounts = new TreeSet<>(); + + public Set<CloudAccount> currentAccounts() { return currentAccounts; } + + @Override + public void allowAccessFor(Set<CloudAccount> accounts) { + currentAccounts = new TreeSet<>(accounts); + } + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java index 38c06e4dac2..84aa26b93f3 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/RoutingController.java @@ -20,6 +20,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordData; import com.yahoo.vespa.hosted.controller.api.integration.dns.RecordName; import com.yahoo.vespa.hosted.controller.application.Endpoint; import com.yahoo.vespa.hosted.controller.application.Endpoint.Port; +import com.yahoo.vespa.hosted.controller.application.Endpoint.Scope; import com.yahoo.vespa.hosted.controller.application.EndpointId; import com.yahoo.vespa.hosted.controller.application.EndpointList; import com.yahoo.vespa.hosted.controller.application.SystemApplication; @@ -115,7 +116,7 @@ public class RoutingController { if (!policy.status().isActive()) continue; RoutingMethod routingMethod = controller.zoneRegistry().routingMethod(policy.id().zone()); endpoints.addAll(policy.zoneEndpointsIn(controller.system(), routingMethod, controller.zoneRegistry())); - endpoints.add(policy.regionEndpointIn(controller.system(), routingMethod)); + endpoints.add(policy.regionEndpointIn(controller.system(), routingMethod, controller.zoneRegistry())); } return EndpointList.copyOf(endpoints); } @@ -158,23 +159,21 @@ public class RoutingController { } // Add application endpoints for (var declaredEndpoint : deploymentSpec.endpoints()) { - Map<ZoneId, Map<DeploymentId, Integer>> deployments = declaredEndpoint.targets().stream() - .collect(groupingBy(t -> ZoneId.from(Environment.prod, t.region()), - toMap(t -> new DeploymentId(application.id().instance(t.instance()), - ZoneId.from(Environment.prod, t.region())), - t -> t.weight()))); - - deployments.forEach((zone, weightedInstances) -> { - // Application endpoints are only supported when using direct routing methods - RoutingMethod routingMethod = usesSharedRouting(zone) ? RoutingMethod.sharedLayer4 : RoutingMethod.exclusive; - endpoints.add(Endpoint.of(application.id()) - .targetApplication(EndpointId.of(declaredEndpoint.endpointId()), - ClusterSpec.Id.from(declaredEndpoint.containerId()), - weightedInstances) - .routingMethod(routingMethod) - .on(Port.fromRoutingMethod(routingMethod)) - .in(controller.system())); - }); + Map<DeploymentId, Integer> deployments = declaredEndpoint.targets().stream() + .collect(toMap(t -> new DeploymentId(application.id().instance(t.instance()), + ZoneId.from(Environment.prod, t.region())), + t -> t.weight())); + + ZoneId zone = deployments.keySet().iterator().next().zoneId(); // Where multiple zones are possible, they all have the same routing method. + // Application endpoints are only supported when using direct routing methods + RoutingMethod routingMethod = usesSharedRouting(zone) ? RoutingMethod.sharedLayer4 : RoutingMethod.exclusive; + endpoints.add(Endpoint.of(application.id()) + .targetApplication(EndpointId.of(declaredEndpoint.endpointId()), + ClusterSpec.Id.from(declaredEndpoint.containerId()), + deployments) + .routingMethod(routingMethod) + .on(Port.fromRoutingMethod(routingMethod)) + .in(controller.system())); } return EndpointList.copyOf(endpoints); } @@ -236,6 +235,7 @@ public class RoutingController { .on(Port.tls()) .in(controller.system()); endpointDnsNames.add(endpoint.dnsName()); + if (endpoint.scope() == Scope.application) endpointDnsNames.add(endpoint.legacyRegionalDnsName()); } return Collections.unmodifiableList(endpointDnsNames); } @@ -313,6 +313,9 @@ public class RoutingController { controller.nameServiceForwarder().createRecord( new Record(Record.Type.CNAME, RecordName.from(endpoint.dnsName()), RecordData.fqdn(vipHostname)), Priority.normal); + controller.nameServiceForwarder().createRecord( + new Record(Record.Type.CNAME, RecordName.from(endpoint.legacyRegionalDnsName()), RecordData.fqdn(vipHostname)), + Priority.normal); } Map<ClusterSpec.Id, EndpointList> applicationEndpointsByCluster = applicationEndpoints.groupingBy(Endpoint::cluster); for (var kv : applicationEndpointsByCluster.entrySet()) { @@ -325,7 +328,7 @@ public class RoutingController { if (matchingTarget.isEmpty()) throw new IllegalStateException("No target found routing to " + deployment + " in " + endpoint); containerEndpoints.add(new ContainerEndpoint(clusterId.value(), asString(Endpoint.Scope.application), - List.of(endpoint.dnsName()), + List.of(endpoint.dnsName(), endpoint.legacyRegionalDnsName()), OptionalInt.of(matchingTarget.get().weight()), endpoint.routingMethod())); } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java index 88366466289..cbac700a9a0 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/application/Endpoint.java @@ -20,9 +20,11 @@ import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; +import static java.util.Comparator.comparing; + /** * Represents an application or instance endpoint in hosted Vespa. - * + * <p> * This encapsulates the logic for building URLs and DNS names for applications in all hosted Vespa systems. * * @author mpolden @@ -38,13 +40,14 @@ public class Endpoint { private final ClusterSpec.Id cluster; private final Optional<InstanceName> instance; private final URI url; + private final URI legacyRegionalUrl; private final List<Target> targets; private final Scope scope; private final boolean legacy; private final RoutingMethod routingMethod; private Endpoint(TenantAndApplicationId application, Optional<InstanceName> instanceName, EndpointId id, - ClusterSpec.Id cluster, URI url, List<Target> targets, Scope scope, Port port, boolean legacy, + ClusterSpec.Id cluster, URI url, URI legacyRegionalUrl, List<Target> targets, Scope scope, Port port, boolean legacy, RoutingMethod routingMethod, boolean certificateName) { Objects.requireNonNull(application, "application must be non-null"); Objects.requireNonNull(instanceName, "instanceName must be non-null"); @@ -58,6 +61,7 @@ public class Endpoint { this.cluster = requireCluster(cluster, certificateName); this.instance = requireInstance(instanceName, scope); this.url = url; + this.legacyRegionalUrl = legacyRegionalUrl; this.targets = List.copyOf(requireTargets(targets, application, instanceName, scope, certificateName)); this.scope = requireScope(scope, routingMethod); this.legacy = legacy; @@ -96,6 +100,12 @@ public class Endpoint { return url.getAuthority().replaceAll(":.*", ""); } + /** Returns the legacy DNS name with region, for application endpoints */ + public String legacyRegionalDnsName() { + if (scope != Scope.application) throw new IllegalStateException("legacy regional URL is only for application scope endpoints, not " + this); + return legacyRegionalUrl.getAuthority().replaceAll(":.*", ""); + } + /** Returns the target(s) to which this routes traffic */ public List<Target> targets() { return targets; @@ -160,7 +170,8 @@ public class Endpoint { } private static URI createUrl(String name, TenantAndApplicationId application, Optional<InstanceName> instance, - List<Target> targets, Scope scope, SystemName system, Port port, boolean legacy) { + List<Target> targets, Scope scope, SystemName system, Port port, boolean legacyRegionalUrl) { + String separator = "."; String portPart = port.isDefault() ? "" : ":" + port.port; return URI.create("https://" + @@ -171,8 +182,8 @@ public class Endpoint { separator + sanitize(application.tenant().value()) + "." + - scopePart(scope, targets, system, legacy) + - dnsSuffix(system, legacy) + + scopePart(scope, targets, system, legacyRegionalUrl) + + dnsSuffix(system) + portPart + "/"); } @@ -186,13 +197,14 @@ public class Endpoint { return name + separator; } - private static String scopePart(Scope scope, List<Target> targets, SystemName system, boolean legacy) { - String scopeSymbol = scopeSymbol(scope, system); + private static String scopePart(Scope scope, List<Target> targets, SystemName system, boolean legacyRegion) { + String scopeSymbol = scopeSymbol(scope, system, legacyRegion); if (scope == Scope.global) return scopeSymbol; + if (scope == Scope.application && ! legacyRegion) return scopeSymbol; - ZoneId zone = targets.get(0).deployment().zoneId(); + ZoneId zone = targets.stream().map(target -> target.deployment.zoneId()).min(comparing(ZoneId::value)).get(); String region = zone.region().value(); - boolean skipEnvironment = zone.environment().isProduction() && (system.isPublic() || !legacy); + boolean skipEnvironment = zone.environment().isProduction(); String environment = skipEnvironment ? "" : "." + zone.environment().value(); if (system.isPublic()) { return region + environment + "." + scopeSymbol; @@ -200,20 +212,21 @@ public class Endpoint { return region + (scopeSymbol.isEmpty() ? "" : "-" + scopeSymbol) + environment; } - private static String scopeSymbol(Scope scope, SystemName system) { + private static String scopeSymbol(Scope scope, SystemName system, boolean legacyRegion) { + if (legacyRegion) return "r"; if (system.isPublic()) { return switch (scope) { case zone -> "z"; case weighted -> "w"; case global -> "g"; - case application -> "r"; + case application -> "a"; }; } return switch (scope) { case zone -> ""; case weighted -> "w"; case global -> "global"; - case application -> "r"; + case application -> "a"; }; } @@ -230,18 +243,15 @@ public class Endpoint { } /** Returns the DNS suffix used for endpoints in given system */ - private static String dnsSuffix(SystemName system, boolean legacy) { + private static String dnsSuffix(SystemName system) { switch (system) { case cd, main -> { - if (legacy) return YAHOO_DNS_SUFFIX; return OATH_DNS_SUFFIX; } case Public -> { - if (legacy) throw new IllegalArgumentException("No legacy DNS suffix declared for system " + system); return PUBLIC_DNS_SUFFIX; } case PublicCd -> { - if (legacy) throw new IllegalArgumentException("No legacy DNS suffix declared for system " + system); return PUBLIC_CD_DNS_SUFFIX; } default -> throw new IllegalArgumentException("No DNS suffix declared for system " + system); @@ -250,7 +260,7 @@ public class Endpoint { /** Returns the DNS suffix used for internal names (i.e. names not exposed to tenants) in given system */ public static String internalDnsSuffix(SystemName system) { - String suffix = dnsSuffix(system, false); + String suffix = dnsSuffix(system); if (system.isPublic()) { // Certificate provider requires special approval for three-level DNS names, e.g. foo.vespa-app.cloud. // To avoid this in public we always add an extra level. @@ -578,12 +588,22 @@ public class Endpoint { Objects.requireNonNull(scope, "scope must be non-null"), Objects.requireNonNull(system, "system must be non-null"), Objects.requireNonNull(port, "port must be non-null"), - legacy); + false); + URI legacyRegionalUrl = scope != Scope.application ? null + : createUrl(endpointOrClusterAsString(endpointId, cluster), + Objects.requireNonNull(application, "application must be non-null"), + Objects.requireNonNull(instance, "instance must be non-null"), + Objects.requireNonNull(targets, "targets must be non-null"), + Objects.requireNonNull(scope, "scope must be non-null"), + Objects.requireNonNull(system, "system must be non-null"), + Objects.requireNonNull(port, "port must be non-null"), + true); return new Endpoint(application, instance, endpointId, cluster, url, + legacyRegionalUrl, targets, scope, port, diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java index 9c6ab32a338..89ca31105bb 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/ControllerMaintenance.java @@ -78,6 +78,7 @@ public class ControllerMaintenance extends AbstractComponent { maintainers.add(new UserManagementMaintainer(controller, intervals.userManagementMaintainer, controller.serviceRegistry().roleMaintainer())); maintainers.add(new BillingDatabaseMaintainer(controller, intervals.billingDatabaseMaintainer)); maintainers.add(new MeteringMonitorMaintainer(controller, intervals.meteringMonitorMaintainer, controller.serviceRegistry().resourceDatabase(), metric)); + maintainers.add(new EnclaveAccessMaintainer(controller, intervals.defaultInterval)); } public Upgrader upgrader() { return upgrader; } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java new file mode 100644 index 00000000000..d9576f4e176 --- /dev/null +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainer.java @@ -0,0 +1,43 @@ +package com.yahoo.vespa.hosted.controller.maintenance; + +import com.yahoo.config.provision.CloudAccount; +import com.yahoo.vespa.hosted.controller.Controller; +import com.yahoo.vespa.hosted.controller.tenant.Tenant; + +import java.time.Duration; +import java.util.HashSet; +import java.util.Set; +import java.util.logging.Logger; + +import static java.util.logging.Level.WARNING; + +public class EnclaveAccessMaintainer extends ControllerMaintainer { + + private static final Logger logger = Logger.getLogger(EnclaveAccessMaintainer.class.getName()); + + EnclaveAccessMaintainer(Controller controller, Duration interval) { + super(controller, interval); + } + + @Override + protected double maintain() { + try { + controller().serviceRegistry().enclaveAccessService().allowAccessFor(externalAccounts()); + return 1; + } + catch (RuntimeException e) { + logger.log(WARNING, "Failed sharing AMIs", e); + return 0; + } + } + + private Set<CloudAccount> externalAccounts() { + Set<CloudAccount> accounts = new HashSet<>(); + for (Tenant tenant : controller().tenants().asList()) + accounts.addAll(controller().applications().accountsOf(tenant.name())); + + return accounts; + } + + +} diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java index 27247c065ed..b0d16126600 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicies.java @@ -215,7 +215,7 @@ public class RoutingPolicies { for (var policy : policies) { if (policy.dnsZone().isEmpty() && policy.canonicalName().isPresent()) continue; if (controller.zoneRegistry().routingMethod(policy.id().zone()) != RoutingMethod.exclusive) continue; - Endpoint endpoint = policy.regionEndpointIn(controller.system(), RoutingMethod.exclusive); + Endpoint endpoint = policy.regionEndpointIn(controller.system(), RoutingMethod.exclusive, controller.zoneRegistry()); var zonePolicy = db.readZoneRoutingPolicy(policy.id().zone()); long weight = 1; if (isConfiguredOut(zonePolicy, policy, inactiveZones)) { @@ -289,12 +289,10 @@ public class RoutingPolicies { activeTargets.addAll(inactiveTargets); inactiveTargets.clear(); } + targetsByEndpoint.forEach((applicationEndpoint, targets) -> { - ZoneId targetZone = applicationEndpoint.targets().stream() - .map(Endpoint.Target::deployment) - .map(DeploymentId::zoneId) - .findFirst() - .get(); + // Where multiple zones are permitted, they all have the same routing policy, and nameServiceForwarder (below). + ZoneId targetZone = applicationEndpoint.targets().iterator().next().deployment().zoneId(); Set<AliasTarget> aliasTargets = new LinkedHashSet<>(); Set<DirectTarget> directTargets = new LinkedHashSet<>(); for (Target target : targets) { @@ -305,23 +303,28 @@ public class RoutingPolicies { if ( ! aliasTargets.isEmpty()) { nameServiceForwarderIn(targetZone).createAlias( RecordName.from(applicationEndpoint.dnsName()), aliasTargets, Priority.normal); + nameServiceForwarderIn(targetZone).createAlias( + RecordName.from(applicationEndpoint.legacyRegionalDnsName()), aliasTargets, Priority.normal); } if ( ! directTargets.isEmpty()) { nameServiceForwarderIn(targetZone).createDirect( RecordName.from(applicationEndpoint.dnsName()), directTargets, Priority.normal); + nameServiceForwarderIn(targetZone).createDirect( + RecordName.from(applicationEndpoint.legacyRegionalDnsName()), directTargets, Priority.normal); } }); inactiveTargetsByEndpoint.forEach((applicationEndpoint, targets) -> { - ZoneId targetZone = applicationEndpoint.targets().stream() - .map(Endpoint.Target::deployment) - .map(DeploymentId::zoneId) - .findFirst() - .get(); + // Where multiple zones are permitted, they all have the same routing policy, and nameServiceForwarder. + ZoneId targetZone = applicationEndpoint.targets().iterator().next().deployment().zoneId(); targets.forEach(target -> { nameServiceForwarderIn(targetZone).removeRecords(target.type(), RecordName.from(applicationEndpoint.dnsName()), target.data(), Priority.normal); + nameServiceForwarderIn(targetZone).removeRecords(target.type(), + RecordName.from(applicationEndpoint.legacyRegionalDnsName()), + target.data(), + Priority.normal); }); }); } @@ -377,9 +380,8 @@ public class RoutingPolicies { .not().matching(policy -> activeIds.contains(policy.id())); for (var policy : removable) { for (var endpoint : policy.zoneEndpointsIn(controller.system(), RoutingMethod.exclusive, controller.zoneRegistry())) { - var dnsName = endpoint.dnsName(); nameServiceForwarderIn(allocation.deployment.zoneId()).removeRecords(Record.Type.CNAME, - RecordName.from(dnsName), + RecordName.from(endpoint.dnsName()), Priority.normal); } newPolicies.remove(policy.id()); @@ -424,14 +426,22 @@ public class RoutingPolicies { for (Endpoint endpoint : endpoints) { if (policy.canonicalName().isPresent()) { forwarder.removeRecords(Record.Type.ALIAS, - RecordName.from(endpoint.dnsName()), - RecordData.fqdn(policy.canonicalName().get().value()), - Priority.normal); + RecordName.from(endpoint.dnsName()), + RecordData.fqdn(policy.canonicalName().get().value()), + Priority.normal); + forwarder.removeRecords(Record.Type.ALIAS, + RecordName.from(endpoint.legacyRegionalDnsName()), + RecordData.fqdn(policy.canonicalName().get().value()), + Priority.normal); } else { forwarder.removeRecords(Record.Type.DIRECT, - RecordName.from(endpoint.dnsName()), - RecordData.from(policy.ipAddress().get()), - Priority.normal); + RecordName.from(endpoint.dnsName()), + RecordData.from(policy.ipAddress().get()), + Priority.normal); + forwarder.removeRecords(Record.Type.DIRECT, + RecordName.from(endpoint.legacyRegionalDnsName()), + RecordData.from(policy.ipAddress().get()), + Priority.normal); } } } diff --git a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java index 04c32590a4c..6ae729a3c02 100644 --- a/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java +++ b/controller-server/src/main/java/com/yahoo/vespa/hosted/controller/routing/RoutingPolicy.java @@ -96,12 +96,12 @@ public record RoutingPolicy(RoutingPolicyId id, /** Returns the zone endpoints of this */ public List<Endpoint> zoneEndpointsIn(SystemName system, RoutingMethod routingMethod, ZoneRegistry zoneRegistry) { DeploymentId deployment = new DeploymentId(id.owner(), id.zone()); - return List.of(endpoint(routingMethod).target(id.cluster(), deployment).in(system)); + return List.of(endpoint(routingMethod, zoneRegistry).target(id.cluster(), deployment).in(system)); } /** Returns the region endpoint of this */ - public Endpoint regionEndpointIn(SystemName system, RoutingMethod routingMethod) { - return endpoint(routingMethod).targetRegion(id.cluster(), id.zone()).in(system); + public Endpoint regionEndpointIn(SystemName system, RoutingMethod routingMethod, ZoneRegistry zoneRegistry) { + return endpoint(routingMethod, zoneRegistry).targetRegion(id.cluster(), id.zone()).in(system); } @Override @@ -125,7 +125,7 @@ public record RoutingPolicy(RoutingPolicyId id, id.zone().value()); } - private Endpoint.EndpointBuilder endpoint(RoutingMethod routingMethod) { + private Endpoint.EndpointBuilder endpoint(RoutingMethod routingMethod, ZoneRegistry zones) { return Endpoint.of(id.owner()) .on(Port.fromRoutingMethod(routingMethod)) .routingMethod(routingMethod); diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java index c87a4e490b9..978587d9c5c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/ControllerTest.java @@ -664,22 +664,22 @@ public class ControllerTest { ZoneId east1a = ZoneId.from("prod", "aws-us-east-1a"); ZoneId east1b = ZoneId.from("prod", "aws-us-east-1b"); // Expected container endpoints are passed to each deployment - Map<DeploymentId, Map<String, Integer>> deploymentEndpoints = Map.of( + Map<DeploymentId, Map<List<String>, Integer>> deploymentEndpoints = Map.of( new DeploymentId(beta, east3), Map.of(), - new DeploymentId(main, east3), Map.of("e.app1.tenant1.us-east-3-r.vespa.oath.cloud", 3), - new DeploymentId(beta, west1), Map.of("d.app1.tenant1.us-west-1-r.vespa.oath.cloud", 3), - new DeploymentId(main, west1), Map.of("d.app1.tenant1.us-west-1-r.vespa.oath.cloud", 7), - new DeploymentId(beta, east1a), Map.of("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", 2, - "b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", 1), - new DeploymentId(main, east1a), Map.of("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", 8, - "b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", 1), - new DeploymentId(beta, east1b), Map.of("c.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud", 4), - new DeploymentId(main, east1b), Map.of("a.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud", 1) + new DeploymentId(main, east3), Map.of(List.of("e.app1.tenant1.a.vespa.oath.cloud", "e.app1.tenant1.us-east-3-r.vespa.oath.cloud"), 3), + new DeploymentId(beta, west1), Map.of(List.of("d.app1.tenant1.a.vespa.oath.cloud", "d.app1.tenant1.us-west-1-r.vespa.oath.cloud"), 3), + new DeploymentId(main, west1), Map.of(List.of("d.app1.tenant1.a.vespa.oath.cloud", "d.app1.tenant1.us-west-1-r.vespa.oath.cloud"), 7), + new DeploymentId(beta, east1a), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud", "a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), 2, + List.of("b.app1.tenant1.a.vespa.oath.cloud", "b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), 1), + new DeploymentId(main, east1a), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud", "a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), 8, + List.of("b.app1.tenant1.a.vespa.oath.cloud", "b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), 1), + new DeploymentId(beta, east1b), Map.of(List.of("c.app1.tenant1.a.vespa.oath.cloud", "c.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud"), 4), + new DeploymentId(main, east1b), Map.of(List.of("a.app1.tenant1.a.vespa.oath.cloud", "a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), 1) ); deploymentEndpoints.forEach((deployment, endpoints) -> { Set<ContainerEndpoint> expected = endpoints.entrySet().stream() .map(kv -> new ContainerEndpoint("default", "application", - List.of(kv.getKey()), + kv.getKey(), OptionalInt.of(kv.getValue()), tester.controller().zoneRegistry().routingMethod(deployment.zoneId()))) .collect(Collectors.toSet()); @@ -704,13 +704,37 @@ public class ControllerTest { RecordName.from("main.app1.tenant1.aws-us-east-1b.vespa.oath.cloud"), RecordData.from("lb-0--tenant1.app1.main--prod.aws-us-east-1b.")), new Record(Record.Type.ALIAS, + RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/2")), + new Record(Record.Type.ALIAS, + RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/8")), + new Record(Record.Type.ALIAS, + RecordName.from("a.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1b/dns-zone-1/prod.aws-us-east-1b/1")), + new Record(Record.Type.ALIAS, + RecordName.from("b.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/1")), + new Record(Record.Type.ALIAS, + RecordName.from("b.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/1")), + new Record(Record.Type.ALIAS, + RecordName.from("c.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1b/dns-zone-1/prod.aws-us-east-1b/4")), + new Record(Record.Type.CNAME, + RecordName.from("d.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("vip.prod.us-west-1.")), + new Record(Record.Type.CNAME, + RecordName.from("e.app1.tenant1.a.vespa.oath.cloud"), + RecordData.from("vip.prod.us-east-3.")), + new Record(Record.Type.ALIAS, RecordName.from("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), RecordData.from("weighted/lb-0--tenant1.app1.beta--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/2")), new Record(Record.Type.ALIAS, RecordName.from("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1a/dns-zone-1/prod.aws-us-east-1a/8")), new Record(Record.Type.ALIAS, - RecordName.from("a.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud"), + RecordName.from("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), RecordData.from("weighted/lb-0--tenant1.app1.main--prod.aws-us-east-1b/dns-zone-1/prod.aws-us-east-1b/1")), new Record(Record.Type.ALIAS, RecordName.from("b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud"), @@ -732,12 +756,11 @@ public class ControllerTest { .scope(Endpoint.Scope.application) .sortedBy(comparing(Endpoint::dnsName)) .mapToList(Endpoint::dnsName); - assertEquals(List.of("a.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", - "a.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud", - "b.app1.tenant1.aws-us-east-1a-r.vespa.oath.cloud", - "c.app1.tenant1.aws-us-east-1b-r.vespa.oath.cloud", - "d.app1.tenant1.us-west-1-r.vespa.oath.cloud", - "e.app1.tenant1.us-east-3-r.vespa.oath.cloud"), + assertEquals(List.of("a.app1.tenant1.a.vespa.oath.cloud", + "b.app1.tenant1.a.vespa.oath.cloud", + "c.app1.tenant1.a.vespa.oath.cloud", + "d.app1.tenant1.a.vespa.oath.cloud", + "e.app1.tenant1.a.vespa.oath.cloud"), endpointDnsNames); } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java index f3324e0c1f3..a76d2eca521 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/application/EndpointTest.java @@ -277,30 +277,65 @@ public class EndpointTest { } @Test + void application_endpoints_legacy_dns_names() { + Map<String, Endpoint> tests = Map.of( + "weighted.a1.t1.us-west-1.r.vespa-app.cloud", + Endpoint.of(app1) + .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"), + Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1)) + .routingMethod(RoutingMethod.exclusive) + .on(Port.tls()) + .in(SystemName.Public), + "weighted.a1.t1.us-west-1.r.cd.vespa-app.cloud", + Endpoint.of(app1) + .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"), + Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1)) + .routingMethod(RoutingMethod.exclusive) + .on(Port.tls()) + .in(SystemName.PublicCd), + "a2.t2.us-east-3-r.vespa.oath.cloud", + Endpoint.of(app2) + .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"), + Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1)) + .routingMethod(RoutingMethod.exclusive) + .on(Port.tls()) + .in(SystemName.main), + "cd.a2.t2.us-east-3-r.vespa.oath.cloud", + Endpoint.of(app2) + .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"), + Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1)) + .routingMethod(RoutingMethod.exclusive) + .on(Port.tls()) + .in(SystemName.cd) + ); + tests.forEach((expected, endpoint) -> assertEquals(expected, endpoint.legacyRegionalDnsName())); + } + + @Test void application_endpoints() { Map<String, Endpoint> tests = Map.of( - "https://weighted.a1.t1.us-west-1.r.vespa-app.cloud/", + "https://weighted.a1.t1.a.vespa-app.cloud/", Endpoint.of(app1) .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"), Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1)) .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.Public), - "https://weighted.a1.t1.us-west-1.r.cd.vespa-app.cloud/", + "https://weighted.a1.t1.a.cd.vespa-app.cloud/", Endpoint.of(app1) .targetApplication(EndpointId.of("weighted"), ClusterSpec.Id.from("qrs"), Map.of(new DeploymentId(app1.instance("i1"), ZoneId.from("prod", "us-west-1")), 1)) .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.PublicCd), - "https://a2.t2.us-east-3-r.vespa.oath.cloud/", + "https://a2.t2.a.vespa.oath.cloud/", Endpoint.of(app2) .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"), Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1)) .routingMethod(RoutingMethod.exclusive) .on(Port.tls()) .in(SystemName.main), - "https://cd.a2.t2.us-east-3-r.vespa.oath.cloud/", + "https://cd.a2.t2.a.vespa.oath.cloud/", Endpoint.of(app2) .targetApplication(EndpointId.defaultId(), ClusterSpec.Id.from("qrs"), Map.of(new DeploymentId(app2.instance("i1"), ZoneId.from("prod", "us-east-3")), 1)) diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java index b06b4eb0cfa..d79a81c7746 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/certificate/EndpointCertificatesTest.java @@ -4,6 +4,7 @@ package com.yahoo.vespa.hosted.controller.certificate; import com.yahoo.config.application.api.DeploymentSpec; import com.yahoo.config.application.api.xml.DeploymentSpecXmlReader; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.Environment; import com.yahoo.config.provision.InstanceName; import com.yahoo.config.provision.RegionName; @@ -25,6 +26,7 @@ import com.yahoo.vespa.hosted.controller.api.integration.certificates.EndpointCe import com.yahoo.vespa.hosted.controller.application.pkg.ApplicationPackage; import com.yahoo.vespa.hosted.controller.deployment.ApplicationPackageBuilder; import com.yahoo.vespa.hosted.controller.integration.SecretStoreMock; +import com.yahoo.vespa.hosted.controller.integration.ZoneApiMock; import com.yahoo.vespa.hosted.controller.persistence.CuratorDb; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -39,6 +41,8 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -136,9 +140,15 @@ public class EndpointCertificatesTest { assertEquals(expectedSans, endpointCertificateMetadata.get().requestedDnsSans()); } + private ControllerTester publicTester() { + ControllerTester publicTester = new ControllerTester(SystemName.Public); + publicTester.zoneRegistry().setZones(tester.zoneRegistry().zones().all().zones()); + return publicTester; + } + @Test void provisions_new_certificate_in_public_prod() { - ControllerTester tester = new ControllerTester(SystemName.Public); + ControllerTester tester = publicTester(); EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock); EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator); List<String> expectedSans = List.of( @@ -238,6 +248,9 @@ public class EndpointCertificatesTest { Instance instance = new Instance(ApplicationId.from("t1", "a1", "default"), Tags.empty()); ZoneId zone1 = ZoneId.from(Environment.prod, RegionName.from("aws-us-east-1c")); ZoneId zone2 = ZoneId.from(Environment.prod, RegionName.from("aws-us-west-2a")); + ControllerTester tester = publicTester(); + tester.zoneRegistry().addZones(ZoneApiMock.newBuilder().with(CloudName.DEFAULT).with(zone1).build(), + ZoneApiMock.newBuilder().with(CloudName.AWS).with(zone2).build()); ApplicationPackage applicationPackage = new ApplicationPackageBuilder() .instances("beta,main") .region(zone1.region()) @@ -251,16 +264,17 @@ public class EndpointCertificatesTest { InstanceName.from("main"), 6), zone2.region().value(), Map.of(InstanceName.from("main"), 2))) .build(); - ControllerTester tester = new ControllerTester(SystemName.Public); EndpointCertificateValidatorImpl endpointCertificateValidator = new EndpointCertificateValidatorImpl(secretStore, clock); EndpointCertificates endpointCertificates = new EndpointCertificates(tester.controller(), endpointCertificateMock, endpointCertificateValidator); - List<String> expectedSans = List.of( + List<String> expectedSans = Stream.of( "vlfms2wpoa4nyrka2s5lktucypjtxkqhv.internal.vespa-app.cloud", "a1.t1.g.vespa-app.cloud", "*.a1.t1.g.vespa-app.cloud", + "a1.t1.a.vespa-app.cloud", "a1.t1.aws-us-west-2a.r.vespa-app.cloud", - "*.a1.t1.aws-us-west-2a.r.vespa-app.cloud", "a1.t1.aws-us-east-1c.r.vespa-app.cloud", + "*.a1.t1.a.vespa-app.cloud", + "*.a1.t1.aws-us-west-2a.r.vespa-app.cloud", "*.a1.t1.aws-us-east-1c.r.vespa-app.cloud", "a1.t1.aws-us-east-1c.z.vespa-app.cloud", "*.a1.t1.aws-us-east-1c.z.vespa-app.cloud", @@ -268,13 +282,13 @@ public class EndpointCertificatesTest { "*.a1.t1.us-east-1.test.z.vespa-app.cloud", "a1.t1.us-east-3.staging.z.vespa-app.cloud", "*.a1.t1.us-east-3.staging.z.vespa-app.cloud" - ); + ).sorted().toList(); Optional<EndpointCertificateMetadata> endpointCertificateMetadata = endpointCertificates.getMetadata(instance, zone1, applicationPackage.deploymentSpec()); assertTrue(endpointCertificateMetadata.isPresent()); assertTrue(endpointCertificateMetadata.get().keyName().matches("vespa.tls.t1.a1.*-key")); assertTrue(endpointCertificateMetadata.get().certName().matches("vespa.tls.t1.a1.*-cert")); assertEquals(0, endpointCertificateMetadata.get().version()); - assertEquals(expectedSans, endpointCertificateMetadata.get().requestedDnsSans()); + assertEquals(expectedSans, endpointCertificateMetadata.get().requestedDnsSans().stream().sorted().toList()); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java index 6493eafcde5..831a79f24b8 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/deployment/TestConfigSerializerTest.java @@ -3,6 +3,7 @@ package com.yahoo.vespa.hosted.controller.deployment; import com.yahoo.component.Version; import com.yahoo.config.provision.ApplicationId; +import com.yahoo.config.provision.CloudName; import com.yahoo.config.provision.ClusterSpec; import com.yahoo.config.provision.SystemName; import com.yahoo.config.provision.zone.ZoneId; @@ -31,22 +32,23 @@ public class TestConfigSerializerTest { @Test void testConfig() throws IOException { ZoneId zone = DeploymentContext.systemTest.zone(); - byte[] json = new TestConfigSerializer(SystemName.PublicCd).configJson(instanceId, - DeploymentContext.systemTest, - true, - Version.fromString("1.2.3"), - RevisionId.forProduction(321), - Instant.ofEpochMilli(222), - Map.of(zone, List.of(Endpoint.of(ApplicationId.defaultId()) - .target(EndpointId.of("ai"), ClusterSpec.Id.from("qrs"), - List.of(new DeploymentId(ApplicationId.defaultId(), - ZoneId.defaultId()))) - .on(Endpoint.Port.tls()) - .in(SystemName.main))), - Map.of(zone, List.of("facts"))); + byte[] json = new TestConfigSerializer(SystemName.PublicCd) + .configJson(instanceId, + DeploymentContext.systemTest, + true, + Version.fromString("1.2.3"), + RevisionId.forProduction(321), + Instant.ofEpochMilli(222), + Map.of(zone, List.of(Endpoint.of(ApplicationId.defaultId()) + .target(EndpointId.of("ai"), ClusterSpec.Id.from("qrs"), + List.of(new DeploymentId(ApplicationId.defaultId(), + ZoneId.defaultId()))) + .on(Endpoint.Port.tls()) + .in(SystemName.main))), + Map.of(zone, List.of("facts"))); byte[] expected = Files.readAllBytes(Paths.get("src/test/resources/testConfig.json")); assertEquals(new String(SlimeUtils.toJsonBytes(SlimeUtils.jsonToSlime(expected))), - new String(json)); + new String(json)); } } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java index af542521b31..46c731e6e49 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/integration/ServiceRegistryMock.java @@ -15,10 +15,9 @@ import com.yahoo.vespa.hosted.controller.api.integration.archive.ArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.archive.MockArchiveService; import com.yahoo.vespa.hosted.controller.api.integration.athenz.AccessControlService; import com.yahoo.vespa.hosted.controller.api.integration.athenz.MockAccessControlService; +import com.yahoo.vespa.hosted.controller.api.integration.aws.MockEnclaveAccessService; import com.yahoo.vespa.hosted.controller.api.integration.aws.MockResourceTagger; import com.yahoo.vespa.hosted.controller.api.integration.aws.MockRoleService; -import com.yahoo.vespa.hosted.controller.api.integration.aws.ResourceTagger; -import com.yahoo.vespa.hosted.controller.api.integration.aws.RoleService; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingController; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClient; import com.yahoo.vespa.hosted.controller.api.integration.billing.BillingDatabaseClientMock; @@ -79,8 +78,9 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg private final MockTesterCloud mockTesterCloud; private final ApplicationStoreMock applicationStoreMock = new ApplicationStoreMock(); private final MockRunDataStore mockRunDataStore = new MockRunDataStore(); + private final MockEnclaveAccessService mockAMIService = new MockEnclaveAccessService(); private final MockResourceTagger mockResourceTagger = new MockResourceTagger(); - private final RoleService roleService = new MockRoleService(); + private final MockRoleService roleService = new MockRoleService(); private final BillingController billingController = new MockBillingController(clock); private final ArtifactRegistryMock containerRegistry = new ArtifactRegistryMock(); private final NoopTenantSecretService tenantSecretService = new NoopTenantSecretService(); @@ -206,12 +206,17 @@ public class ServiceRegistryMock extends AbstractComponent implements ServiceReg } @Override - public ResourceTagger resourceTagger() { + public MockResourceTagger resourceTagger() { return mockResourceTagger; } @Override - public RoleService roleService() { + public MockEnclaveAccessService enclaveAccessService() { + return mockAMIService; + } + + @Override + public MockRoleService roleService() { return roleService; } diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java new file mode 100644 index 00000000000..f5188d52db6 --- /dev/null +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/maintenance/EnclaveAccessMaintainerTest.java @@ -0,0 +1,39 @@ +package com.yahoo.vespa.hosted.controller.maintenance; + +import com.yahoo.config.provision.CloudAccount; +import com.yahoo.vespa.flags.PermanentFlags; +import com.yahoo.vespa.hosted.controller.ControllerTester; +import com.yahoo.vespa.hosted.controller.api.integration.aws.MockEnclaveAccessService; +import org.junit.jupiter.api.Test; + +import java.time.Duration; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author jonmv + */ +class EnclaveAccessMaintainerTest { + + @Test + void test() { + ControllerTester tester = new ControllerTester(); + MockEnclaveAccessService amis = tester.serviceRegistry().enclaveAccessService(); + EnclaveAccessMaintainer sharer = new EnclaveAccessMaintainer(tester.controller(), Duration.ofMinutes(1)); + assertEquals(Set.of(), amis.currentAccounts()); + + assertEquals(1, sharer.maintain()); + assertEquals(Set.of(), amis.currentAccounts()); + + tester.createTenant("tanten"); + assertEquals(1, sharer.maintain()); + assertEquals(Set.of(), amis.currentAccounts()); + + tester.flagSource().withListFlag(PermanentFlags.CLOUD_ACCOUNTS.id(), List.of("123123123123", "321321321321"), String.class); + assertEquals(1, sharer.maintain()); + assertEquals(Set.of(CloudAccount.from("123123123123"), CloudAccount.from("321321321321")), amis.currentAccounts()); + } + +} diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java index 0f03333146f..d6da677cb27 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/ApplicationApiTest.java @@ -114,10 +114,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; public class ApplicationApiTest extends ControllerContainerTest { private static final String responseFiles = "src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/"; - private static final String pemPublicKey = "-----BEGIN PUBLIC KEY-----\n" + - "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9\n" + - "z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw==\n" + - "-----END PUBLIC KEY-----\n"; + private static final String pemPublicKey = """ + -----BEGIN PUBLIC KEY----- + MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEuKVFA8dXk43kVfYKzkUqhEY2rDT9 + z/4jKSTHwbYR8wdsOSrJGVEUPbS2nguIJ64OJH7gFnxM6sxUVj+Nm2HlXw== + -----END PUBLIC KEY----- + """; private static final String quotedPemPublicKey = pemPublicKey.replaceAll("\\n", "\\\\n"); private static final String accessDenied = "{\n \"code\" : 403,\n \"message\" : \"Access denied\"\n}"; diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json index 37bd69d5863..cc42b3e006c 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/deployment.json @@ -24,7 +24,7 @@ { "cluster": "foo", "tls": true, - "url": "https://a0.application1.tenant1.us-central-1-r.vespa.oath.cloud/", + "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/", "scope": "application", "routingMethod": "sharedLayer4", "legacy": false diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json index d78ab67dcc5..f37112ea887 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/instance1-recursive.json @@ -111,7 +111,7 @@ { "cluster": "foo", "tls": true, - "url": "https://a0.application1.tenant1.us-central-1-r.vespa.oath.cloud/", + "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/", "scope": "application", "routingMethod": "sharedLayer4", "legacy": false diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json index 8d76d54458d..4458040858b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/recursive-root.json @@ -118,7 +118,7 @@ { "cluster": "foo", "tls": true, - "url": "https://a0.application1.tenant1.us-central-1-r.vespa.oath.cloud/", + "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/", "scope": "application", "routingMethod": "sharedLayer4", "legacy": false diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json index b9bf714d362..ea025b60d1b 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/application/responses/tenant1-recursive.json @@ -117,7 +117,7 @@ { "cluster": "foo", "tls": true, - "url": "https://a0.application1.tenant1.us-central-1-r.vespa.oath.cloud/", + "url": "https://a0.application1.tenant1.a.vespa.oath.cloud/", "scope": "application", "routingMethod": "sharedLayer4", "legacy": false diff --git a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json index 48771ac064b..18c7322b885 100644 --- a/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json +++ b/controller-server/src/test/java/com/yahoo/vespa/hosted/controller/restapi/controller/responses/maintenance.json @@ -49,6 +49,9 @@ "name": "DeploymentUpgrader" }, { + "name": "EnclaveAccessMaintainer" + }, + { "name": "EndpointCertificateMaintainer" }, { diff --git a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java index 274b0d114ed..0ccdfddf1b8 100644 --- a/flags/src/main/java/com/yahoo/vespa/flags/Flags.java +++ b/flags/src/main/java/com/yahoo/vespa/flags/Flags.java @@ -432,6 +432,13 @@ public class Flags { "Takes effect immediately?", ZONE_ID, APPLICATION_ID); + public static final UnboundBooleanFlag USE_LOCKS_IN_FILEDISTRIBUTION = defineFeatureFlag( + "use-locks-in-filedistribution", false, + List.of("hmusum"), "2022-11-16", "2023-01-31", + "If true, use locks when writing and deleting file references.", + "Takes effect immediately", + ZONE_ID, APPLICATION_ID); + /** WARNING: public for testing: All flags should be defined in {@link Flags}. */ public static UnboundBooleanFlag defineFeatureFlag(String flagId, boolean defaultValue, List<String> owners, String createdAt, String expiresAt, String description, diff --git a/jdisc-security-filters/pom.xml b/jdisc-security-filters/pom.xml index a452327f6ff..652b864747d 100644 --- a/jdisc-security-filters/pom.xml +++ b/jdisc-security-filters/pom.xml @@ -53,6 +53,16 @@ <artifactId>junit-jupiter</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-http</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-util</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java index 752f1026f3d..f7a2e41dae4 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzAuthorizationFilterTest.java @@ -9,6 +9,7 @@ import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.jdisc.http.filter.security.athenz.AthenzAuthorizationFilterConfig.EnabledCredentials; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.SubjectAlternativeName; @@ -267,14 +268,11 @@ public class AthenzAuthorizationFilterTest { } private static DiscFilterRequest createRequest(ZToken roleToken, AthenzAccessToken accessToken, X509Certificate clientCert) { - DiscFilterRequest request = mock(DiscFilterRequest.class); - when(request.getHeader(HEADER_NAME)).thenReturn(roleToken != null ? roleToken.getRawToken() : null); - when(request.getHeader(AthenzAccessToken.HTTP_HEADER_NAME)).thenReturn(accessToken != null ? "Bearer " + accessToken.value() : null); - when(request.getMethod()).thenReturn("GET"); - when(request.getRequestURI()).thenReturn("/my/path"); - when(request.getQueryString()).thenReturn(null); - when(request.getClientCertificateChain()).thenReturn(clientCert != null ? List.of(clientCert) : List.of()); - return request; + var builder = FilterTestUtils.newRequestBuilder().withUri("https://localhost/my/path"); + if (roleToken != null) builder.withHeader(HEADER_NAME, roleToken.getRawToken()); + if (accessToken != null) builder.withHeader(AthenzAccessToken.HTTP_HEADER_NAME, accessToken.value()); + if (clientCert != null) builder.withClientCertificate(clientCert); + return builder.build(); } private static AthenzAuthorizationFilter createFilter(Zpe zpe, List<EnabledCredentials.Enum> enabledCredentials) { @@ -298,7 +296,7 @@ public class AthenzAuthorizationFilterTest { } private static void assertAuthorizationResult(DiscFilterRequest request, Type expectedResult) { - verify(request).setAttribute(RESULT_ATTRIBUTE, expectedResult.name()); + assertEquals(expectedResult.name(), request.getAttribute(RESULT_ATTRIBUTE)); } private static void assertStatusCode(MockResponseHandler responseHandler, int statusCode) { @@ -308,7 +306,7 @@ public class AthenzAuthorizationFilterTest { } private static void assertMatchedCredentialType(DiscFilterRequest request, EnabledCredentials.Enum expectedType) { - verify(request).setAttribute(MATCHED_CREDENTIAL_TYPE_ATTRIBUTE, expectedType.name()); + assertEquals(expectedType.name(), request.getAttribute(MATCHED_CREDENTIAL_TYPE_ATTRIBUTE)); } private static void assertRequestNotFiltered(MockResponseHandler responseHandler) { @@ -316,7 +314,7 @@ public class AthenzAuthorizationFilterTest { } private static void assertMatchedRole(DiscFilterRequest request, AthenzRole role) { - verify(request).setAttribute(MATCHED_ROLE_ATTRIBUTE, role.roleName()); + assertEquals(role.roleName(), request.getAttribute(MATCHED_ROLE_ATTRIBUTE)); } private static void assertErrorMessage(MockResponseHandler responseHandler, String errorMessage) { diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java index 0b04993a723..6ee589c1908 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/athenz/AthenzPrincipalFilterTest.java @@ -6,6 +6,7 @@ import com.yahoo.jdisc.handler.ContentChannel; import com.yahoo.jdisc.handler.ReadableContentChannel; import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.X509CertificateBuilder; @@ -28,16 +29,11 @@ import java.util.Objects; import static com.yahoo.jdisc.Response.Status.UNAUTHORIZED; import static com.yahoo.security.SignatureAlgorithm.SHA256_WITH_ECDSA; -import static java.util.Collections.emptyList; -import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; /** * @author bjorncs @@ -49,8 +45,7 @@ public class AthenzPrincipalFilterTest { @Test void missing_certificate_is_unauthorized() { - DiscFilterRequest request = createRequestMock(); - when(request.getClientCertificateChain()).thenReturn(emptyList()); + DiscFilterRequest request = FilterTestUtils.newRequestBuilder().build(); ResponseHandlerMock responseHandler = new ResponseHandlerMock(); @@ -62,8 +57,7 @@ public class AthenzPrincipalFilterTest { @Test void certificate_is_accepted() { - DiscFilterRequest request = createRequestMock(); - when(request.getClientCertificateChain()).thenReturn(singletonList(CERTIFICATE)); + DiscFilterRequest request = FilterTestUtils.newRequestBuilder().withClientCertificate(CERTIFICATE).build(); ResponseHandlerMock responseHandler = new ResponseHandlerMock(); @@ -75,15 +69,14 @@ public class AthenzPrincipalFilterTest { } private void assertAuthenticated(DiscFilterRequest request, AthenzPrincipal expectedPrincipal) { - verify(request).setUserPrincipal(expectedPrincipal); - verify(request).setAttribute(AthenzPrincipalFilter.RESULT_PRINCIPAL, expectedPrincipal); + assertEquals(expectedPrincipal, request.getUserPrincipal()); + assertEquals(expectedPrincipal, request.getAttribute(AthenzPrincipalFilter.RESULT_PRINCIPAL)); } @Test void no_response_produced_when_passthrough_mode_is_enabled() { - DiscFilterRequest request = createRequestMock(); - when(request.getClientCertificateChain()).thenReturn(emptyList()); + DiscFilterRequest request = FilterTestUtils.newRequestBuilder().build(); ResponseHandlerMock responseHandler = new ResponseHandlerMock(); @@ -93,10 +86,6 @@ public class AthenzPrincipalFilterTest { assertNull(responseHandler.response); } - private DiscFilterRequest createRequestMock() { - return mock(DiscFilterRequest.class); - } - private AthenzPrincipalFilter createFilter(boolean passthroughModeEnabled) { return new AthenzPrincipalFilter(passthroughModeEnabled); } @@ -105,7 +94,7 @@ public class AthenzPrincipalFilterTest { assertNotNull(responseHandler.response); assertEquals(UNAUTHORIZED, responseHandler.response.getStatus()); assertTrue(responseHandler.getResponseContent().contains(expectedMessageSubstring)); - verify(request).setAttribute(AthenzPrincipalFilter.RESULT_ERROR_CODE_ATTRIBUTE, UNAUTHORIZED); + assertEquals(UNAUTHORIZED, request.getAttribute(AthenzPrincipalFilter.RESULT_ERROR_CODE_ATTRIBUTE)); } private static class ResponseHandlerMock implements ResponseHandler { diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBaseTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBaseTest.java index 34db051aa28..fe530ed90cb 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBaseTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/base/JsonSecurityRequestFilterBaseTest.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.yahoo.container.jdisc.RequestHandlerTestDriver; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import org.junit.jupiter.api.Test; import java.io.IOException; @@ -13,7 +14,6 @@ import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.mockito.Mockito.mock; /** * @author bjorncs @@ -26,7 +26,7 @@ public class JsonSecurityRequestFilterBaseTest { void filter_renders_errors_as_json() throws IOException { int statusCode = 403; String message = "Forbidden"; - DiscFilterRequest request = mock(DiscFilterRequest.class); + DiscFilterRequest request = FilterTestUtils.newRequestBuilder().build(); SimpleSecurityRequestFilter filter = new SimpleSecurityRequestFilter(new JsonSecurityRequestFilterBase.ErrorResponse(statusCode, message)); RequestHandlerTestDriver.MockResponseHandler responseHandler = new RequestHandlerTestDriver.MockResponseHandler(); diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cors/CorsPreflightRequestFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cors/CorsPreflightRequestFilterTest.java index 7ba050b7cc0..576b04e23b6 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cors/CorsPreflightRequestFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/cors/CorsPreflightRequestFilterTest.java @@ -8,6 +8,7 @@ import com.yahoo.jdisc.handler.ResponseHandler; import com.yahoo.jdisc.http.filter.DiscFilterRequest; import com.yahoo.jdisc.http.filter.SecurityRequestFilter; import com.yahoo.jdisc.http.filter.security.cors.CorsFilterConfig.Builder; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import org.junit.jupiter.api.Test; import java.util.Arrays; @@ -19,7 +20,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; /** @@ -70,10 +70,7 @@ public class CorsPreflightRequestFilterTest { } private static DiscFilterRequest newOptionsRequest(String origin) { - DiscFilterRequest request = mock(DiscFilterRequest.class); - when(request.getHeader("Origin")).thenReturn(origin); - when(request.getMethod()).thenReturn(OPTIONS.name()); - return request; + return FilterTestUtils.newRequestBuilder().withHeader("Origin", origin).withMethod(OPTIONS).build(); } private static CorsPreflightRequestFilter newRequestFilter(String... allowedOriginUrls) { diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/LocalhostFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/LocalhostFilterTest.java index aaf6ebf1aee..5b9f143a72b 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/LocalhostFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/LocalhostFilterTest.java @@ -4,14 +4,11 @@ package com.yahoo.jdisc.http.filter.security.misc; import com.yahoo.container.jdisc.RequestHandlerTestDriver; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import java.net.URI; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.when; /** * @author mpolden @@ -34,12 +31,10 @@ public class LocalhostFilterTest { } private static DiscFilterRequest createRequest(String remoteAddr, String localAddr) { - DiscFilterRequest request = Mockito.mock(DiscFilterRequest.class); - when(request.getRemoteAddr()).thenReturn(remoteAddr); - when(request.getLocalAddr()).thenReturn(localAddr); - when(request.getMethod()).thenReturn("GET"); - when(request.getUri()).thenReturn(URI.create("http://localhost:8080/")); - return request; + return FilterTestUtils.newRequestBuilder() + .withUri("http://%s:8080/".formatted(localAddr)) + .withRemoteAddress(remoteAddr, 12345) + .build(); } private static void assertUnauthorized(DiscFilterRequest request) { diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java index 95bc1f92572..2dd243618c9 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/misc/VespaTlsFilterTest.java @@ -5,16 +5,15 @@ package com.yahoo.jdisc.http.filter.security.misc; import com.yahoo.container.jdisc.RequestHandlerTestDriver; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import com.yahoo.security.KeyAlgorithm; import com.yahoo.security.KeyUtils; import com.yahoo.security.SignatureAlgorithm; import com.yahoo.security.X509CertificateBuilder; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import javax.security.auth.x500.X500Principal; import java.math.BigInteger; -import java.net.URI; import java.security.cert.X509Certificate; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -23,7 +22,6 @@ import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.when; public class VespaTlsFilterTest { @@ -43,11 +41,7 @@ public class VespaTlsFilterTest { } private static DiscFilterRequest createRequest(List<X509Certificate> certChain) { - DiscFilterRequest request = Mockito.mock(DiscFilterRequest.class); - when(request.getClientCertificateChain()).thenReturn(certChain); - when(request.getMethod()).thenReturn("GET"); - when(request.getUri()).thenReturn(URI.create("http://localhost:8080/")); - return request; + return FilterTestUtils.newRequestBuilder().withClientCertificate(certChain).withUri("http://localhost:8080/").build(); } private static void assertForbidden(DiscFilterRequest request) { diff --git a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/rule/RuleBasedRequestFilterTest.java b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/rule/RuleBasedRequestFilterTest.java index 4ad593efe82..c4a78a2d962 100644 --- a/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/rule/RuleBasedRequestFilterTest.java +++ b/jdisc-security-filters/src/test/java/com/yahoo/jdisc/http/filter/security/rule/RuleBasedRequestFilterTest.java @@ -8,13 +8,13 @@ import com.yahoo.container.jdisc.RequestHandlerTestDriver.MockResponseHandler; import com.yahoo.jdisc.Metric; import com.yahoo.jdisc.Response; import com.yahoo.jdisc.http.filter.DiscFilterRequest; +import com.yahoo.jdisc.http.filter.util.FilterTestUtils; import com.yahoo.vespa.config.jdisc.http.filter.RuleBasedFilterConfig; import com.yahoo.vespa.config.jdisc.http.filter.RuleBasedFilterConfig.DefaultRule; import com.yahoo.vespa.config.jdisc.http.filter.RuleBasedFilterConfig.Rule; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; import java.util.List; import java.util.Set; @@ -25,7 +25,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; /** * @author bjorncs @@ -218,10 +217,7 @@ class RuleBasedRequestFilterTest { } private static DiscFilterRequest request(String method, String uri) { - DiscFilterRequest request = mock(DiscFilterRequest.class); - when(request.getMethod()).thenReturn(method); - when(request.getUri()).thenReturn(URI.create(uri)); - return request; + return FilterTestUtils.newRequestBuilder().withMethod(method).withUri(uri).build(); } private static void assertAllowed(MockResponseHandler handler, Metric metric) { diff --git a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java index f4cd35bc5bd..4a8353d92ec 100644 --- a/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java +++ b/node-admin/src/main/java/com/yahoo/vespa/hosted/node/admin/maintenance/coredump/CoredumpHandler.java @@ -29,7 +29,6 @@ import com.yahoo.vespa.hosted.node.admin.task.util.file.MakeDirectory; import com.yahoo.vespa.hosted.node.admin.task.util.file.UnixPath; import com.yahoo.vespa.hosted.node.admin.task.util.fs.ContainerPath; -import javax.crypto.CipherOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; @@ -49,7 +48,6 @@ import java.util.function.Supplier; import java.util.logging.Logger; import java.util.regex.Pattern; import java.util.stream.IntStream; -import java.util.stream.Stream; import static com.yahoo.vespa.hosted.node.admin.task.util.file.FileFinder.nameEndsWith; import static com.yahoo.vespa.hosted.node.admin.task.util.file.FileFinder.nameMatches; @@ -275,7 +273,7 @@ public class CoredumpHandler { static OutputStream maybeWrapWithEncryption(OutputStream wrappedStream, Optional<SecretSharedKey> sharedCoreKey) { return sharedCoreKey - .map(key -> (OutputStream)new CipherOutputStream(wrappedStream, SharedKeyGenerator.makeAesGcmEncryptionCipher(key))) + .map(key -> SharedKeyGenerator.makeAesGcmEncryptionCipher(key).wrapOutputStream(wrappedStream)) .orElse(wrappedStream); } diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RebuildingOsUpgrader.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RebuildingOsUpgrader.java index 6b61c864a0c..bb9d28d48f9 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RebuildingOsUpgrader.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RebuildingOsUpgrader.java @@ -68,8 +68,10 @@ public class RebuildingOsUpgrader implements OsUpgrader { private List<Node> rebuildableHosts(OsVersionTarget target, NodeList allNodes, Instant now) { NodeList hostsOfTargetType = allNodes.nodeType(target.nodeType()); if (softRebuild) { - // Soft rebuild is enabled so this should only act on hosts with remote storage - hostsOfTargetType = hostsOfTargetType.storageType(NodeResources.StorageType.remote); + // Soft rebuild is enabled so this should only act on hosts with remote storage and on x86-64 + // TODO(mpolden): Rebuild arm64 hosts as well if image permissions can be fixed + hostsOfTargetType = hostsOfTargetType.matching(node -> node.resources().storageType() == NodeResources.StorageType.remote && + node.resources().architecture() == NodeResources.Architecture.x86_64); } int rebuildLimit = rebuildLimit(target.nodeType(), hostsOfTargetType); diff --git a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringOsUpgrader.java b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringOsUpgrader.java index 72ab26f71a7..acdb689b809 100644 --- a/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringOsUpgrader.java +++ b/node-repository/src/main/java/com/yahoo/vespa/hosted/provision/os/RetiringOsUpgrader.java @@ -62,8 +62,9 @@ public class RetiringOsUpgrader implements OsUpgrader { private NodeList candidates(Instant instant, OsVersionTarget target, NodeList allNodes) { NodeList activeNodes = allNodes.state(Node.State.active).nodeType(target.nodeType()); if (softRebuild) { - // Soft rebuild is enabled, so this should only act on hosts with local storage - activeNodes = activeNodes.storageType(NodeResources.StorageType.local); + // Soft rebuild is enabled, so this should only act on hosts with local storage, or non-x86-64 + activeNodes = activeNodes.matching(node -> node.resources().storageType() == NodeResources.StorageType.local || + node.resources().architecture() != NodeResources.Architecture.x86_64); } if (activeNodes.isEmpty()) return NodeList.of(); diff --git a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisionerTest.java b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisionerTest.java index df316567422..64b32276ca2 100644 --- a/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisionerTest.java +++ b/node-repository/src/test/java/com/yahoo/vespa/hosted/provision/maintenance/HostResumeProvisionerTest.java @@ -75,7 +75,6 @@ public class HostResumeProvisionerTest { Supplier<NodeList> provisioning = () -> tester.nodeRepository().nodes().list(Node.State.provisioned).nodeType(NodeType.host); assertEquals(1, provisioning.get().size()); - provisioning.get().forEach(h -> System.out.println(h.hostname() + " " + h.ipConfig())); hostResumeProvisioner.maintain(); assertTrue("No IP addresses written as DNS updates are failing", diff --git a/parent/pom.xml b/parent/pom.xml index b17a56c9dc8..1c9f4d387bb 100644 --- a/parent/pom.xml +++ b/parent/pom.xml @@ -911,6 +911,11 @@ </dependency> <dependency> <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-http</artifactId> + <version>${jetty.version}</version> + </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> <artifactId>jetty-server</artifactId> <version>${jetty.version}</version> </dependency> diff --git a/searchlib/src/vespa/searchlib/memoryindex/field_inverter.cpp b/searchlib/src/vespa/searchlib/memoryindex/field_inverter.cpp index 72260072842..e8d9259ad20 100644 --- a/searchlib/src/vespa/searchlib/memoryindex/field_inverter.cpp +++ b/searchlib/src/vespa/searchlib/memoryindex/field_inverter.cpp @@ -16,6 +16,7 @@ #include <vespa/searchcommon/common/schema.h> #include <vespa/searchlib/common/sort.h> #include <vespa/searchlib/util/url.h> +#include <vespa/vespalib/datastore/aligner.h> #include <vespa/vespalib/text/utf8.h> #include <vespa/vespalib/util/stringfmt.h> #include <vespa/vespalib/stllike/hash_map.hpp> @@ -50,6 +51,7 @@ using index::Schema; using search::index::schema::CollectionType; using search::util::URL; using vespalib::make_string; +using vespalib::datastore::Aligner; namespace documentinverterkludge::linguistics { @@ -252,15 +254,15 @@ FieldInverter::saveWord(const vespalib::stringref word) return 0u; } - const size_t fullyPaddedSize = (wordsSize + 4 + len + 1 + 3) & ~3; + const size_t unpadded_size = wordsSize + 4 + len + 1; + const size_t fullyPaddedSize = Aligner<4>::align(unpadded_size); _words.reserve(vespalib::roundUp2inN(fullyPaddedSize)); _words.resize(fullyPaddedSize); char * buf = &_words[0] + wordsSize; memset(buf, 0, 4); memcpy(buf + 4, word.data(), len); - uint32_t *lastWord = reinterpret_cast<uint32_t *>(buf + 4 + (len & ~0x3)); - *lastWord &= (0xffffff >> ((3 - (len & 3)) << 3)); //only on little endian machiness !! + memset(buf + 4 + len, 0, fullyPaddedSize - unpadded_size + 1); uint32_t wordRef = (wordsSize + 4) >> 2; // assert(wordRef != 0); diff --git a/security-utils/src/main/java/com/yahoo/security/AeadCipher.java b/security-utils/src/main/java/com/yahoo/security/AeadCipher.java new file mode 100644 index 00000000000..598f5d01db7 --- /dev/null +++ b/security-utils/src/main/java/com/yahoo/security/AeadCipher.java @@ -0,0 +1,44 @@ +// Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.security; + +import org.bouncycastle.crypto.io.CipherInputStream; +import org.bouncycastle.crypto.io.CipherOutputStream; +import org.bouncycastle.crypto.modes.AEADBlockCipher; + +import java.io.InputStream; +import java.io.OutputStream; + +/** + * AEAD cipher wrapper to hide the underlying crypto provider used. + * + * @author vekterli + */ +public class AeadCipher { + + private final AEADBlockCipher cipher; + + private AeadCipher(AEADBlockCipher cipher) { + this.cipher = cipher; + } + + static AeadCipher of(AEADBlockCipher cipher) { + return new AeadCipher(cipher); + } + + /** + * Returns a wrapping <code>OutputStream</code> that, depending on the cipher mode, either + * encrypts or decrypts all data that is written to it before passing it on to <code>out</code>. + */ + public OutputStream wrapOutputStream(OutputStream out) { + return new CipherOutputStream(out, cipher); + } + + /** + * Returns a wrapping <code>InputStream</code> that, depending on the cipher mode, either + * encrypts or decrypts all data that is read from the underlying input stream. + */ + public InputStream wrapInputStream(InputStream in) { + return new CipherInputStream(in, cipher); + } + +} diff --git a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java index 66a87a94707..16bd82e2af3 100644 --- a/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java +++ b/security-utils/src/main/java/com/yahoo/security/SharedKeyGenerator.java @@ -6,15 +6,14 @@ import com.yahoo.security.hpke.Ciphersuite; import com.yahoo.security.hpke.Hpke; import com.yahoo.security.hpke.Kdf; import com.yahoo.security.hpke.Kem; +import org.bouncycastle.crypto.engines.AESEngine; +import org.bouncycastle.crypto.modes.GCMBlockCipher; +import org.bouncycastle.crypto.params.AEADParameters; +import org.bouncycastle.crypto.params.KeyParameter; -import javax.crypto.Cipher; import javax.crypto.KeyGenerator; -import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; -import javax.crypto.spec.GCMParameterSpec; import javax.crypto.spec.SecretKeySpec; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; @@ -93,33 +92,29 @@ public class SharedKeyGenerator { 'h','e','r','e','B','d','r','a','g','o','n','s' // Nothing up my sleeve! }; - private static Cipher makeAesGcmCipher(SecretSharedKey secretSharedKey, int cipherMode) { - try { - var cipher = Cipher.getInstance(AES_GCM_ALGO_SPEC); - var gcmSpec = new GCMParameterSpec(AES_GCM_AUTH_TAG_BITS, FIXED_96BIT_IV_FOR_SINGLE_USE_KEY); - cipher.init(cipherMode, secretSharedKey.secretKey(), gcmSpec); - return cipher; - } catch (NoSuchAlgorithmException | NoSuchPaddingException - | InvalidKeyException | InvalidAlgorithmParameterException e) { - throw new RuntimeException(e); - } + private static AeadCipher makeAesGcmCipher(SecretSharedKey secretSharedKey, boolean forEncryption) { + var aeadParams = new AEADParameters(new KeyParameter(secretSharedKey.secretKey().getEncoded()), + AES_GCM_AUTH_TAG_BITS, FIXED_96BIT_IV_FOR_SINGLE_USE_KEY); + var cipher = new GCMBlockCipher(new AESEngine()); + cipher.init(forEncryption, aeadParams); + return AeadCipher.of(cipher); } /** - * Creates an AES-GCM Cipher that can be used to encrypt arbitrary plaintext. + * Creates an AES-GCM cipher that can be used to encrypt arbitrary plaintext. * * The given secret key MUST NOT be used to encrypt more than one plaintext. */ - public static Cipher makeAesGcmEncryptionCipher(SecretSharedKey secretSharedKey) { - return makeAesGcmCipher(secretSharedKey, Cipher.ENCRYPT_MODE); + public static AeadCipher makeAesGcmEncryptionCipher(SecretSharedKey secretSharedKey) { + return makeAesGcmCipher(secretSharedKey, true); } /** - * Creates an AES-GCM Cipher that can be used to decrypt ciphertext that was previously + * Creates an AES-GCM cipher that can be used to decrypt ciphertext that was previously * encrypted with the given secret key. */ - public static Cipher makeAesGcmDecryptionCipher(SecretSharedKey secretSharedKey) { - return makeAesGcmCipher(secretSharedKey, Cipher.DECRYPT_MODE); + public static AeadCipher makeAesGcmDecryptionCipher(SecretSharedKey secretSharedKey) { + return makeAesGcmCipher(secretSharedKey, false); } } diff --git a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java index 23e22345cc6..aede100574d 100644 --- a/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java +++ b/security-utils/src/test/java/com/yahoo/security/SharedKeyTest.java @@ -3,8 +3,6 @@ package com.yahoo.security; import org.junit.jupiter.api.Test; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -142,7 +140,7 @@ public class SharedKeyTest { static byte[] streamEncryptString(String data, SecretSharedKey secretSharedKey) throws IOException { var cipher = SharedKeyGenerator.makeAesGcmEncryptionCipher(secretSharedKey); var outStream = new ByteArrayOutputStream(); - try (var cipherStream = new CipherOutputStream(outStream, cipher)) { + try (var cipherStream = cipher.wrapOutputStream(outStream)) { cipherStream.write(data.getBytes(StandardCharsets.UTF_8)); cipherStream.flush(); } @@ -154,7 +152,7 @@ public class SharedKeyTest { var inStream = new ByteArrayInputStream(encrypted); var total = ByteBuffer.allocate(encrypted.length); // Assume decrypted form can't be _longer_ byte[] tmp = new byte[8]; // short buf to test chunking - try (var cipherStream = new CipherInputStream(inStream, cipher)) { + try (var cipherStream = cipher.wrapInputStream(inStream)) { while (true) { int read = cipherStream.read(tmp); if (read == -1) { @@ -180,4 +178,32 @@ public class SharedKeyTest { assertEquals(terrifyingSecret, decrypted); } + // javax.crypto.CipherOutputStream swallows exceptions caused by MAC failures in cipher + // decryption mode (!) and must therefore _not_ be used for this purpose. This is documented, + // but still very surprising behavior. + @Test + void cipher_output_stream_tag_mismatch_is_not_swallowed() throws Exception { + var receiverKeyPair = KeyUtils.generateX25519KeyPair(); + var myShared = SharedKeyGenerator.generateForReceiverPublicKey(receiverKeyPair.getPublic(), KEY_ID_1); + String plaintext = "...hello world?"; + byte[] encrypted = streamEncryptString(plaintext, myShared); + // Corrupt MAC tag in ciphertext + encrypted[encrypted.length - 1] ^= 0x80; + // We don't necessarily know _which_ exception is thrown, but one _should_ be thrown! + assertThrows(Exception.class, () -> doOutputStreamCipherDecrypt(myShared, encrypted)); + // Also try with corrupted ciphertext (pre MAC tag) + encrypted[encrypted.length - 1] ^= 0x80; // Flip MAC bit back to correct state + encrypted[encrypted.length - 17] ^= 0x80; // Pre 128-bit MAC tag + assertThrows(Exception.class, () -> doOutputStreamCipherDecrypt(myShared, encrypted)); + } + + private static void doOutputStreamCipherDecrypt(SecretSharedKey myShared, byte[] encrypted) throws Exception { + var cipher = SharedKeyGenerator.makeAesGcmDecryptionCipher(myShared); + var outStream = new ByteArrayOutputStream(); + try (var cipherStream = cipher.wrapOutputStream(outStream)) { + cipherStream.write(encrypted); + cipherStream.flush(); + } + } + } diff --git a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java index 051189c20b6..87d3cb4d9f0 100644 --- a/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java +++ b/vespaclient-java/src/main/java/com/yahoo/vespa/security/tool/crypto/CipherUtils.java @@ -1,8 +1,8 @@ // Copyright Yahoo. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.vespa.security.tool.crypto; -import javax.crypto.Cipher; -import javax.crypto.CipherOutputStream; +import com.yahoo.security.AeadCipher; + import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -19,11 +19,11 @@ public class CipherUtils { * * @param input source stream to read from * @param output destination stream to write to - * @param cipher a Cipher in either ENCRYPT or DECRYPT mode + * @param cipher an {@link AeadCipher} created with for either encryption or decryption * @throws IOException if any file operation fails */ - public static void streamEncipher(InputStream input, OutputStream output, Cipher cipher) throws IOException { - try (var cipherStream = new CipherOutputStream(output, cipher)) { + public static void streamEncipher(InputStream input, OutputStream output, AeadCipher cipher) throws IOException { + try (var cipherStream = cipher.wrapOutputStream(output)) { input.transferTo(cipherStream); cipherStream.flush(); } |