Skip to content

Commit

Permalink
feat(auth): implement auto-detection for k8s serviceaccount token (#383)
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewazores authored May 31, 2024
1 parent 8a54c72 commit 96f491d
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,9 @@ and how it advertises itself to a Cryostat server instance. Properties that requ
- [ ] `cryostat.agent.instance-id` [`String`]: a unique ID for this agent instance. This will be used to uniquely identify the agent in the Cryostat discovery database, as well as to unambiguously match its encrypted stored credentials. The default is a random UUID string. It is not recommended to override this value.
- [ ] `cryostat.agent.hostname` [`String`]: the hostname for this application instance. This will be used for the published JMX connection URL. If not provided then the default is to attempt to resolve the localhost hostname.
- [ ] `cryostat.agent.realm` [`String`]: the Cryostat Discovery API "realm" that this agent belongs to. This should be unique per agent instance. The default is the value of `cryostat.agent.app.name`.
- [ ] `cryostat.agent.authorization` [`String`]: Authorization header value to include with API requests to the Cryostat server, ex. `Bearer abcd1234`. Default `None`.
- [ ] `cryostat.agent.authorization` [`String`]: `Authorization` header value to include with API requests to the Cryostat server, ex. `Bearer abcd1234`. Takes precedence over `cryostat.agent.authorization.type` and `cryostat.agent.authorization.value`. Defaults to the empty string, so `cryostat.agent.authorization.type` and `cryostat.agent.authorization.value` are used instead.
- [ ] `cryostat.agent.authorization.type` [`String`]: may be `basic`, `bearer`, `kubernetes`, `none`, or `auto`. Each performs a mapping of the `cryostat.agent.authorization.value` to produce an `Authorization` header (see above). `basic` encodes the value using base64 to produce a `Basic base64(value)` header, `bearer` directly embeds the value into a `Bearer value` header, `kubernetes` reads the value as a file location to produce a `Bearer fileAsString(value)` header, `none` produces no header. Default `auto`, which tries to do `kubernetes` first and falls back on `none`.
- [ ] `cryostat.agent.authorization.value` [`String`]: the value to map into an `Authorization` header. If the `cryostat.agent.authorization.type` is `basic` then this should be the unencoded basic credentials, ex. `user:pass`. If `bearer` then it should be the token to be presented. If `kubernetes` it should be the filesystem path to the service account token secret file. If `none` it is ignored. Default `/var/run/secrets/kubernetes.io/serviceaccount/token`, the standard location for Kubernetes serviceaccount token secret files.
- [ ] `cryostat.agent.webclient.ssl.trust-all` [`boolean`]: Control whether the agent trusts all certificates presented by the Cryostat server. Default `false`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.ssl.verify-hostname` [`boolean`]: Control whether the agent verifies hostnames on certificates presented by the Cryostat server. Default `true`. This should only be overridden for development and testing purposes, never in production.
- [ ] `cryostat.agent.webclient.connect.timeout-ms` [`long`]: the duration in milliseconds to wait for HTTP requests to the Cryostat server to connect. Default `1000`.
Expand Down
80 changes: 80 additions & 0 deletions src/main/java/io/cryostat/agent/AuthorizationType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.cryostat.agent;

import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.Base64;
import java.util.function.Function;

import io.cryostat.agent.util.StringUtils;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public enum AuthorizationType implements Function<String, String> {
NONE(v -> null),
BEARER(v -> String.format("Bearer %s", v)),
BASIC(
v ->
String.format(
"Basic %s",
Base64.getEncoder()
.encodeToString(v.getBytes(StandardCharsets.UTF_8)))),
KUBERNETES(
v -> {
try {
File file = new File(v);
String token = Files.readString(file.toPath()).strip();
return String.format("Bearer %s", token);
} catch (IOException ioe) {
Logger log = LoggerFactory.getLogger(AuthorizationType.class);
log.warn(String.format("Failed to read serviceaccount token from %s", v), ioe);
return null;
}
}),
AUTO(
v -> {
String k8s = KUBERNETES.fn.apply(v);
if (StringUtils.isNotBlank(k8s)) {
return k8s;
}
return NONE.fn.apply(v);
}),
;

private final Function<String, String> fn;

private AuthorizationType(Function<String, String> fn) {
this.fn = fn;
}

@Override
public String apply(String in) {
return fn.apply(in);
}

public static AuthorizationType fromString(String s) {
for (AuthorizationType t : AuthorizationType.values()) {
if (t.name().toLowerCase().equals(s.toLowerCase())) {
return t;
}
}
return NONE;
}
}
27 changes: 25 additions & 2 deletions src/main/java/io/cryostat/agent/ConfigModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import java.security.PrivilegedExceptionAction;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Predicate;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -54,6 +55,10 @@ public abstract class ConfigModule {
public static final String CRYOSTAT_AGENT_CALLBACK = "cryostat.agent.callback";
public static final String CRYOSTAT_AGENT_REALM = "cryostat.agent.realm";
public static final String CRYOSTAT_AGENT_AUTHORIZATION = "cryostat.agent.authorization";
public static final String CRYOSTAT_AGENT_AUTHORIZATION_TYPE =
"cryostat.agent.authorization.type";
public static final String CRYOSTAT_AGENT_AUTHORIZATION_VALUE =
"cryostat.agent.authorization.value";

public static final String CRYOSTAT_AGENT_WEBCLIENT_SSL_TRUST_ALL =
"cryostat.agent.webclient.ssl.trust-all";
Expand Down Expand Up @@ -163,8 +168,26 @@ public static String provideCryostatAgentRealm(
@Provides
@Singleton
@Named(CRYOSTAT_AGENT_AUTHORIZATION)
public static String provideCryostatAgentAuthorization(Config config) {
return config.getValue(CRYOSTAT_AGENT_AUTHORIZATION, String.class);
public static Optional<String> provideCryostatAgentAuthorization(
Config config,
AuthorizationType authorizationType,
@Named(CRYOSTAT_AGENT_AUTHORIZATION_VALUE) Optional<String> authorizationValue) {
Optional<String> opt = config.getOptionalValue(CRYOSTAT_AGENT_AUTHORIZATION, String.class);
return opt.or(() -> authorizationValue.map(authorizationType::apply));
}

@Provides
@Singleton
public static AuthorizationType provideCryostatAgentAuthorizationType(Config config) {
return AuthorizationType.fromString(
config.getValue(CRYOSTAT_AGENT_AUTHORIZATION_TYPE, String.class));
}

@Provides
@Singleton
@Named(CRYOSTAT_AGENT_AUTHORIZATION_VALUE)
public static Optional<String> provideCryostatAgentAuthorizationValue(Config config) {
return config.getOptionalValue(CRYOSTAT_AGENT_AUTHORIZATION_VALUE, String.class);
}

@Provides
Expand Down
17 changes: 13 additions & 4 deletions src/main/java/io/cryostat/agent/MainModule.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -46,6 +49,7 @@
import dagger.Module;
import dagger.Provides;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.http.Header;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.impl.client.HttpClientBuilder;
Expand Down Expand Up @@ -152,14 +156,20 @@ public X509Certificate[] getAcceptedIssuers() {
@Singleton
public static HttpClient provideHttpClient(
SSLContext sslContext,
@Named(ConfigModule.CRYOSTAT_AGENT_AUTHORIZATION) String authorization,
AuthorizationType authorizationType,
@Named(ConfigModule.CRYOSTAT_AGENT_AUTHORIZATION) Optional<String> authorization,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_SSL_VERIFY_HOSTNAME)
boolean verifyHostname,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_CONNECT_TIMEOUT_MS) int connectTimeout,
@Named(ConfigModule.CRYOSTAT_AGENT_WEBCLIENT_RESPONSE_TIMEOUT_MS) int responseTimeout) {
Set<Header> headers = new HashSet<>();
authorization
.filter(Objects::nonNull)
.map(v -> new BasicHeader("Authorization", v))
.ifPresent(headers::add);
HttpClientBuilder builder =
HttpClients.custom()
.setDefaultHeaders(Set.of(new BasicHeader("Authorization", authorization)))
.setDefaultHeaders(headers)
.setSSLContext(sslContext)
.setDefaultRequestConfig(
RequestConfig.custom()
Expand Down Expand Up @@ -192,8 +202,7 @@ public static CryostatClient provideCryostatClient(
@Named(JVM_ID) String jvmId,
@Named(ConfigModule.CRYOSTAT_AGENT_APP_NAME) String appName,
@Named(ConfigModule.CRYOSTAT_AGENT_BASEURI) URI baseUri,
@Named(ConfigModule.CRYOSTAT_AGENT_REALM) String realm,
@Named(ConfigModule.CRYOSTAT_AGENT_AUTHORIZATION) String authorization) {
@Named(ConfigModule.CRYOSTAT_AGENT_REALM) String realm) {
return new CryostatClient(
executor, objectMapper, http, instanceId, jvmId, appName, baseUri, realm);
}
Expand Down
4 changes: 3 additions & 1 deletion src/main/resources/META-INF/microprofile-config.properties
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ cryostat.agent.webserver.credentials.user=user
cryostat.agent.webserver.credentials.pass.hash-function=SHA-256
cryostat.agent.webserver.credentials.pass.length=24

cryostat.agent.authorization=None
cryostat.agent.authorization=
cryostat.agent.authorization.type=auto
cryostat.agent.authorization.value=/var/run/secrets/kubernetes.io/serviceaccount/token
cryostat.agent.callback=
cryostat.agent.realm=

Expand Down

0 comments on commit 96f491d

Please sign in to comment.