Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: TokenRefreshInterceptor throws when running incluster #3445

Merged
merged 5 commits into from
Sep 21, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
### 5.8-SNAPSHOT

#### Bugs
* Fix #3445: TokenRefreshInterceptor throws when running incluster config

#### Improvements
#### Dependency Upgrade
#### New Features
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ System properties are preferred over environment variables. The following system
| `kubernetes.certs.client.key.passphrase` / `KUBERNETES_CERTS_CLIENT_KEY_PASSPHRASE` | | |
| `kubernetes.auth.basic.username` / `KUBERNETES_AUTH_BASIC_USERNAME` | | |
| `kubernetes.auth.basic.password` / `KUBERNETES_AUTH_BASIC_PASSWORD` | | |
| `kubernetes.auth.serviceAccount.token` / `KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN` | Name of the service account token file | `/var/run/secrets/kubernetes.io/serviceaccount/token`|
| `kubernetes.auth.tryKubeConfig` / `KUBERNETES_AUTH_TRYKUBECONFIG` | Configure client using Kubernetes config | `true` |
| `kubeconfig` / `KUBECONFIG` | Name of the kubernetes config file to read | `~/.kube/config` |
| `kubernetes.auth.tryServiceAccount` / `KUBERNETES_AUTH_TRYSERVICEACCOUNT` | Configure client from Service account | `true` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;

import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.AuthProviderConfig;
import io.fabric8.kubernetes.api.model.Cluster;
import io.fabric8.kubernetes.api.model.ConfigBuilder;
import io.fabric8.kubernetes.api.model.Context;
Expand Down Expand Up @@ -83,6 +84,7 @@ public class Config {
public static final String KUBERNETES_AUTH_BASIC_PASSWORD_SYSTEM_PROPERTY = "kubernetes.auth.basic.password";
public static final String KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY = "kubernetes.auth.tryKubeConfig";
public static final String KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY = "kubernetes.auth.tryServiceAccount";
public static final String KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY = "kubernetes.auth.serviceAccount.token";
public static final String KUBERNETES_OAUTH_TOKEN_SYSTEM_PROPERTY = "kubernetes.auth.token";
public static final String KUBERNETES_WATCH_RECONNECT_INTERVAL_SYSTEM_PROPERTY = "kubernetes.watch.reconnectInterval";
public static final String KUBERNETES_WATCH_RECONNECT_LIMIT_SYSTEM_PROPERTY = "kubernetes.watch.reconnectLimit";
Expand Down Expand Up @@ -169,6 +171,7 @@ public class Config {
private String trustStorePassphrase;
private String keyStoreFile;
private String keyStorePassphrase;
private AuthProviderConfig authProvider;

private RequestConfig requestConfig = new RequestConfig();

Expand Down Expand Up @@ -470,30 +473,33 @@ private static boolean tryServiceAccount(Config config) {
LOGGER.debug("Trying to configure client from service account...");
String masterHost = Utils.getSystemPropertyOrEnvVar(KUBERNETES_SERVICE_HOST_PROPERTY, (String) null);
String masterPort = Utils.getSystemPropertyOrEnvVar(KUBERNETES_SERVICE_PORT_PROPERTY, (String) null);
String saTokenPath = Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY, KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH);
String caCertPath = Utils.getSystemPropertyOrEnvVar(KUBERNETES_CA_CERTIFICATE_FILE_SYSTEM_PROPERTY, KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH);

if (masterHost != null && masterPort != null) {
String hostPort = joinHostPort(masterHost, masterPort);
LOGGER.debug("Found service account host and port: {}", hostPort);
config.setMasterUrl("https://" + hostPort);
}
if (Utils.getSystemPropertyOrEnvVar(KUBERNETES_AUTH_TRYSERVICEACCOUNT_SYSTEM_PROPERTY, true)) {
boolean serviceAccountCaCertExists = Files.isRegularFile(new File(KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH).toPath());
boolean serviceAccountCaCertExists = Files.isRegularFile(new File(caCertPath).toPath());
if (serviceAccountCaCertExists) {
LOGGER.debug("Found service account ca cert at: ["+KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH+"].");
config.setCaCertFile(KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH);
LOGGER.debug("Found service account ca cert at: [{}}].", caCertPath);
config.setCaCertFile(caCertPath);
} else {
LOGGER.debug("Did not find service account ca cert at: ["+KUBERNETES_SERVICE_ACCOUNT_CA_CRT_PATH+"].");
LOGGER.debug("Did not find service account ca cert at: [{}}].", caCertPath);
}
try {
String serviceTokenCandidate = new String(Files.readAllBytes(new File(KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH).toPath()));
LOGGER.debug("Found service account token at: ["+KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH+"].");
String serviceTokenCandidate = new String(Files.readAllBytes(new File(saTokenPath).toPath()));
LOGGER.debug("Found service account token at: [{}].", saTokenPath);
config.setOauthToken(serviceTokenCandidate);
String txt = "Configured service account doesn't have access. Service account may have been revoked.";
config.getErrorMessages().put(401, "Unauthorized! " + txt);
config.getErrorMessages().put(403, "Forbidden!" + txt);
return true;
} catch (IOException e) {
// No service account token available...
LOGGER.warn("Error reading service account token from: [{}]. Ignoring.", KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH);
LOGGER.warn("Error reading service account token from: [{}]. Ignoring.", saTokenPath);
}
}
return false;
Expand Down Expand Up @@ -619,6 +625,7 @@ private static boolean loadFromKubeconfig(Config config, String context, String

if (Utils.isNullOrEmpty(config.getOauthToken()) && currentAuthInfo.getAuthProvider() != null) {
if (currentAuthInfo.getAuthProvider().getConfig() != null) {
config.setAuthProvider(currentAuthInfo.getAuthProvider());
if (!Utils.isNullOrEmpty(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN))) {
// GKE token
config.setOauthToken(currentAuthInfo.getAuthProvider().getConfig().get(ACCESS_TOKEN));
Expand Down Expand Up @@ -1341,4 +1348,11 @@ public Readiness getReadiness() {
return Readiness.getInstance();
}

public void setAuthProvider(AuthProviderConfig authProvider) {
this.authProvider = authProvider;
}

public AuthProviderConfig getAuthProvider() {
return authProvider;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,11 @@
*/
package io.fabric8.kubernetes.client.utils;

import io.fabric8.kubernetes.api.model.AuthInfo;
import io.fabric8.kubernetes.api.model.Context;
import io.fabric8.kubernetes.client.Config;
import io.fabric8.kubernetes.client.internal.KubeConfigUtils;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

import java.io.File;
import java.io.IOException;
import java.net.HttpURLConnection;

Expand All @@ -41,30 +37,26 @@ public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) {
io.fabric8.kubernetes.api.model.Config kubeConfig = KubeConfigUtils.parseConfig(new File(Config.getKubeconfigFilename()));
Context currentContext = null;
String currentContextName = null;
String newAccessToken = null;

if (config.getCurrentContext() != null) {
currentContext = config.getCurrentContext().getContext();
currentContextName = config.getCurrentContext().getName();
}
AuthInfo currentAuthInfo = KubeConfigUtils.getUserAuthInfo(kubeConfig, currentContext);
// Check if AuthProvider is set or not
if (currentAuthInfo != null) {
Config newestConfig = Config.autoConfigure(currentContextName);
if (newestConfig.getAuthProvider() != null && newestConfig.getAuthProvider().getName().equalsIgnoreCase("oidc")) {
newAccessToken = OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(newestConfig.getAuthProvider().getConfig());
} else {
newAccessToken = newestConfig.getOauthToken();
}

if (newAccessToken != null) {
response.close();
String newAccessToken;
// Check if AuthProvider is set to oidc
if (currentAuthInfo.getAuthProvider() != null && currentAuthInfo.getAuthProvider().getName().equalsIgnoreCase("oidc")) {
newAccessToken = OpenIDConnectionUtils.resolveOIDCTokenFromAuthConfig(currentAuthInfo.getAuthProvider().getConfig());
} else {
Config newestConfig = Config.autoConfigure(currentContextName);
newAccessToken = newestConfig.getOauthToken();
}
// Delete old Authorization header and append new one
Request authReqWithUpdatedToken = chain.request().newBuilder()
.header("Authorization", "Bearer " + newAccessToken).build();
config.setOauthToken(newAccessToken);
return chain.proceed(authReqWithUpdatedToken);
response = chain.proceed(authReqWithUpdatedToken);
}
}
return response;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
import java.util.Objects;

import static io.fabric8.kubernetes.client.Config.KUBERNETES_KUBECONFIG_FILE;
import static io.fabric8.kubernetes.client.Config.KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY;
import static io.fabric8.kubernetes.client.Config.KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY;

public class TokenRefreshInterceptorTest {

Expand Down Expand Up @@ -64,4 +66,89 @@ public void shouldAutoconfigureAfter401() throws IOException {
System.clearProperty(KUBERNETES_KUBECONFIG_FILE);
}
}

@Test
void shouldReloadInClusterServiceAccount() throws IOException {
try {
// Write service account token file with value "expired" in it,
// Set properties for it to be used instead of kubeconfig.
File tokenFile = Files.createTempFile("test", "token").toFile();
Files.write(tokenFile.toPath(), "expired".getBytes());
System.setProperty(KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY, tokenFile.getAbsolutePath());
System.setProperty(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY, "false");

// Prepare HTTP call that will fail with 401 Unauthorized to trigger service account token reload.
Interceptor.Chain chain = Mockito.mock(Interceptor.Chain.class, Mockito.RETURNS_DEEP_STUBS);
Request req = new Request.Builder().url("http://mock").build();
Mockito.when(chain.request()).thenReturn(req);
final Response.Builder responseBuilder = new Response.Builder()
.request(req)
.protocol(Protocol.HTTP_1_1)
.message("")
.body(ResponseBody.create(MediaType.parse("text"), "foo"));
Mockito.when(chain.proceed(Mockito.any())).thenReturn(
responseBuilder.code(HttpURLConnection.HTTP_UNAUTHORIZED).build(),
responseBuilder.code(HttpURLConnection.HTTP_OK).build());

// The expired token will be read at auto configure.
TokenRefreshInterceptor interceptor = new TokenRefreshInterceptor(Config.autoConfigure(null));

// Write new value to token file to simulate renewal.
Files.write(tokenFile.toPath(), "renewed".getBytes());
interceptor.intercept(chain);

// Make the call and check that renewed token was read at 401 Unauthorized.
Mockito.verify(chain)
.proceed(Mockito.argThat(argument -> "Bearer renewed".equals(argument.header("Authorization"))));
} finally {
// Remove any side effect
System.clearProperty(KUBERNETES_AUTH_SERVICEACCOUNT_TOKEN_FILE_SYSTEM_PROPERTY);
System.clearProperty(KUBERNETES_AUTH_TRYKUBECONFIG_SYSTEM_PROPERTY);
}
}

@Test
void shouldRefreshOIDCToken() throws IOException {
try {
// Prepare kubeconfig for autoconfiguration
File tempFile = Files.createTempFile("test", "kubeconfig").toFile();
Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/test-kubeconfig-oidc")),
Paths.get(tempFile.getPath()), StandardCopyOption.REPLACE_EXISTING);
System.setProperty(KUBERNETES_KUBECONFIG_FILE, tempFile.getAbsolutePath());

// Prepare HTTP call that will fail with 401 Unauthorized to trigger OIDC token renewal.
Interceptor.Chain chain = Mockito.mock(Interceptor.Chain.class, Mockito.RETURNS_DEEP_STUBS);
Request req = new Request.Builder().url("http://mock").build();
Mockito.when(chain.request()).thenReturn(req);
final Response.Builder responseBuilder = new Response.Builder()
.request(req).protocol(Protocol.HTTP_1_1)
.message("")
.body(ResponseBody.create(MediaType.parse("text"), "foo"));
Mockito.when(chain.proceed(Mockito.any())).thenReturn(
responseBuilder.code(HttpURLConnection.HTTP_UNAUTHORIZED).build(),
responseBuilder.code(HttpURLConnection.HTTP_OK).build());

// Loads the initial kubeconfig, including initial token value.
Config config = Config.autoConfigure(null);

// Copy over new config with following auth provider configuration:
// - refresh-token is set to null to avoid real network connection towards
// OIDC provider. This makes it unnecessary to mock the OIDC HTTP client.
// - id-token to set to "renewed". Since the original id-token at autoconfigure
// had different value, we can be used the new value to assert/observe that
// 401 Unauthorized triggers renewal when OIDC auth provider is used.
Files.copy(Objects.requireNonNull(getClass().getResourceAsStream("/test-kubeconfig-tokeninterceptor-oidc")),
Paths.get(tempFile.getPath()), StandardCopyOption.REPLACE_EXISTING);

new TokenRefreshInterceptor(config).intercept(chain);

// Make the call and check that renewed token was read at 401 Unauthorized.
Mockito.verify(chain)
.proceed(Mockito.argThat(argument -> "Bearer renewed".equals(argument.header("Authorization"))));
} finally {
// Remove any side effect.
System.clearProperty(KUBERNETES_KUBECONFIG_FILE);
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
apiVersion: v1
clusters:
- cluster:
certificate-authority: testns/ca.pem
insecure-skip-tls-verify: true
server: https://172.28.128.4:8443
name: 172-28-128-4:8443
contexts:
- context:
cluster: 172-28-128-4:8443
namespace: testns
user: user/172-28-128-4:8443
name: testns/172-28-128-4:8443/user
- context:
cluster: 172-28-128-4:8443
namespace: production
user: root/172-28-128-4:8443
name: production/172-28-128-4:8443/root
- context:
cluster: 172-28-128-4:8443
namespace: production
user: mmosley
name: production/172-28-128-4:8443/mmosley
current-context: production/172-28-128-4:8443/mmosley
kind: Config
preferences: {}
users:
- name: user/172-28-128-4:8443
user:
token: token
- name: root/172-28-128-4:8443
user:
token: supertoken
- name: mmosley
user:
auth-provider:
config:
client-id: kubernetes
client-secret: 1db158f6-177d-4d9c-8a8b-d36869918ec5
id-token: renewed
idp-certificate-authority: /root/ca.pem
idp-issuer-url: https://oidcidp.tremolo.lan:8443/auth/idp/OidcIdP
name: oidc