Skip to content

[JENKINS-73525] add FIPS compliance checks to plugin when running in FIPS mode #50

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

Merged
merged 8 commits into from
Aug 13, 2024
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
6 changes: 3 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
<gitHubRepo>jenkinsci/${project.artifactId}-plugin</gitHubRepo>

<!-- jenkins versions -->
<jenkins.version>2.401.3</jenkins.version>
<bom.artifactId>bom-2.401.x</bom.artifactId>
<bom.version>2745.vc7b_fe4c876fa_</bom.version>
<jenkins.version>2.426.3</jenkins.version>
<bom.artifactId>bom-2.426.x</bom.artifactId>
<bom.version>3208.vb_21177d4b_cd9</bom.version>

<!-- maven plugins versions -->
<maven-coveralls.version>4.3.0</maven-coveralls.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jenkinsci.plugins.kubernetes.auth;

import org.jenkinsci.plugins.kubernetes.credentials.Utils;

/**
* Configuration object for {@link KubernetesAuth} operations.
*/
Expand All @@ -18,6 +20,7 @@ public class KubernetesAuthConfig {
private final boolean skipTlsVerify;

public KubernetesAuthConfig(String serverUrl, String caCertificate, boolean skipTlsVerify) {
Utils.ensureFIPSCompliantRequest(serverUrl, skipTlsVerify);
this.serverUrl = serverUrl;
this.caCertificate = caCertificate;
this.skipTlsVerify = skipTlsVerify;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ private static SSLConnectionSocketFactory getVerifyCertSSLFactory(String hostnam
}

public static HttpClientBuilder getBuilder(URI uri, String caCertData, boolean skipTLSVerify) throws TLSConfigurationError {
Utils.ensureFIPSCompliantURIRequest(uri, skipTLSVerify);
final HttpClientBuilder builder = HttpClients.custom().setRedirectStrategy(NO_HTTP_REDIRECT);

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import net.sf.json.JSONObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.Header;
import org.apache.http.HttpHeaders;
import org.apache.http.NameValuePair;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.CloseableHttpResponse;
Expand Down Expand Up @@ -135,7 +136,9 @@ private synchronized Token refreshToken(String apiServerURL, String caCertData,

final HttpClientBuilder builder = HttpClientWithTLSOptionsFactory.getBuilder(uri, caCertData, skipTLSVerify);
HttpGet authorize = new HttpGet(oauthServerURL + "?client_id=openshift-challenging-client&response_type=token");
authorize.setHeader("Authorization", getBasicAuthenticationHeader(getUsername(), getPassword()));
authorize.setHeader(HttpHeaders.AUTHORIZATION, getBasicAuthenticationHeader(getUsername(), getPassword()));

Utils.ensureFIPSCompliantURIRequest(authorize.getURI(), skipTLSVerify);
final CloseableHttpResponse response = builder.build().execute(authorize);

if (response.getStatusLine().getStatusCode() != 302) {
Expand All @@ -154,6 +157,7 @@ private String getOauthServerUrl(String apiServerURL, String caCertData, boolean
URI uri = new URI(apiServerURL);
final HttpClientBuilder builder = HttpClientWithTLSOptionsFactory.getBuilder(uri, caCertData, skipTLSVerify);
HttpGet discover = new HttpGet(apiServerURL + "/.well-known/oauth-authorization-server");
Utils.ensureFIPSCompliantURIRequest(discover.getURI(), skipTLSVerify);
final CloseableHttpResponse response = builder.build().execute(discover);
return JSONObject.fromObject(EntityUtils.toString(response.getEntity())).getString("authorization_endpoint");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package org.jenkinsci.plugins.kubernetes.credentials;

import jenkins.security.FIPS140;
import org.apache.commons.codec.binary.Base64;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;

public abstract class Utils {

/**
* Error message used to indicate that skipping TLS verification is not accepted in FIPS mode.
*/
public static final String FIPS140_ERROR_MESSAGE =
"Using an insecure connection and/or skipping TLS verification is not accepted in FIPS mode.";

public static String wrapWithMarker(String begin, String end, String encodedBody) {
return new StringBuilder(begin).append("\n")
.append(encodedBody).append("\n")
Expand Down Expand Up @@ -46,4 +53,55 @@ public static String encodeCertificate(Certificate certificate) throws Certifica
public static String encodeKey(Key key) {
return encodeBase64(wrapPrivateKey(Base64.encodeBase64String(key.getEncoded())));
}

/**
* Ensure that the URI request is FIPS compliant for the given HttpUriRequest object and skipTLSVerify option.
* Throw an exception if the request is invalid.
* A request is considered valid if the connection is either using TLS or a local pipe
* and if the TLS verification is not skipped.
* If FIPS mode is not enabled, this method does nothing.
*
* @param uri The request to validate
* @param skipTLSVerify A flag indicating whether to skip TLS verification or not
* @throws IllegalArgumentException If the request is invalid
*/
public static void ensureFIPSCompliantURIRequest(URI uri, boolean skipTLSVerify) {
boolean isInsecure = uri.getScheme().equals("http");
ensureFIPSCompliant(isInsecure, skipTLSVerify);
}

/**
* Ensure that the request is FIPS compliant for the given URL and skipTLSVerify option.
* Throw an exception if the request is invalid.
* A request is considered valid if the connection is either using TLS or a local pipe
* and if the TLS verification is not skipped.
* If FIPS mode is not enabled, this method does nothing.
*
* @param stringRequest The request to validate
* @param skipTLSVerify A flag indicating whether to skip TLS verification or not
* @throws IllegalArgumentException If the request is invalid
*/
public static void ensureFIPSCompliantRequest(String stringRequest, boolean skipTLSVerify) {
boolean isInsecure = stringRequest.startsWith("http://");
ensureFIPSCompliant(isInsecure, skipTLSVerify);
}

/**
* Ensure FIPS compliance based on the following rules:
* <ul>
* <li>Must use a secure connection</li>
* <li>TLS verification is mandatory</li>
* </ul>
* Throw an exception if not compliant.
* If FIPS mode is not enabled, this method does nothing.
*
* @param insecureConnection If the connection is insecure
* @param skipTLSVerify A flag indicating whether to skip TLS verification or not
* @throws IllegalArgumentException If not FIPS compliant
*/
private static void ensureFIPSCompliant(boolean insecureConnection, boolean skipTLSVerify) {
if (FIPS140.useCompliantAlgorithms() && (insecureConnection || skipTLSVerify)) {
throw new IllegalArgumentException(Utils.FIPS140_ERROR_MESSAGE);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.jenkinsci.plugins.kubernetes.auth;

import static org.junit.Assert.fail;

import org.junit.Test;

public abstract class AbstractKubernetesAuthConfigFIPSTest {
protected String scheme;

protected boolean skipTLSVerify;

protected boolean shouldPass;

protected String motivation;

public AbstractKubernetesAuthConfigFIPSTest(
String scheme, boolean skipTLSVerify, boolean shouldPass, String motivation) {
this.scheme = scheme;
this.skipTLSVerify = skipTLSVerify;
this.shouldPass = shouldPass;
this.motivation = motivation;
}

@Test
public void testCreateKubernetesAuthConfig() {
try {
new KubernetesAuthConfig(scheme + "://server", null, skipTLSVerify);
if (!shouldPass) {
fail("This test was expected to fail, reason: " + motivation);
}
} catch (IllegalArgumentException e) {
if (shouldPass) {
fail("This test was expected to pass, reason: " + motivation);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.jenkinsci.plugins.kubernetes.auth;

import java.util.Arrays;
import java.util.Collection;
import jenkins.security.FIPS140;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.jvnet.hudson.test.FlagRule;

@RunWith(Parameterized.class)
public class KubernetesAuthConfigWithFIPSTest extends AbstractKubernetesAuthConfigFIPSTest {
@ClassRule
public static FlagRule<String> fipsFlag = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "true");

public KubernetesAuthConfigWithFIPSTest(
String scheme, boolean skipTLSVerify, boolean shouldPass, String motivation) {
super(scheme, skipTLSVerify, shouldPass, motivation);
}

@Parameterized.Parameters
public static Collection<Object[]> parameters() {
return Arrays.asList(new Object[][] {
// Valid use cases
{"https", false, true, "TLS is used and the TLS verification is not skipped, this should be accepted"},
// Invalid use cases
{"https", true, false, "Skip TLS check is not accepted in FIPS mode"},
{"http", false, false, "TLS is mandatory when in FIPS mode"},
{"http", true, false, "TLS and TLS check are mandatory when in FIPS mode"},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.jenkinsci.plugins.kubernetes.auth;

import java.util.Arrays;
import java.util.Collection;
import jenkins.security.FIPS140;
import org.junit.ClassRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.jvnet.hudson.test.FlagRule;

@RunWith(Parameterized.class)
public class KubernetesAuthConfigWithoutFIPSTest extends AbstractKubernetesAuthConfigFIPSTest {
@ClassRule
public static FlagRule<String> fipsFlag = FlagRule.systemProperty(FIPS140.class.getName() + ".COMPLIANCE", "false");

public KubernetesAuthConfigWithoutFIPSTest(
String scheme, boolean skipTLSVerify, boolean shouldPass, String motivation) {
super(scheme, skipTLSVerify, shouldPass, motivation);
}

@Parameterized.Parameters
public static Collection<Object[]> parameters() {
return Arrays.asList(new Object[][] {
// Valid use cases
{"https", false, true, "Not in FIPS mode, any combination should be valid"},
{"http", false, true, "Not in FIPS mode, any combination should be valid"},
{"http", true, true, "Not in FIPS mode, any combination should be valid"},
{"https", true, true, "Not in FIPS mode, any combination should be valid"},
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package org.jenkinsci.plugins.kubernetes.credentials;

import static org.junit.Assert.fail;

import java.net.URI;
import java.net.URISyntaxException;
import org.junit.Test;

public abstract class AbstractHttpClientWithTLSOptionsFactoryFIPSTest {
protected String scheme;

protected boolean skipTLSVerify;

protected boolean shouldPass;

protected String motivation;

public AbstractHttpClientWithTLSOptionsFactoryFIPSTest(String scheme, boolean skipTLSVerify, boolean shouldPass, String motivation) {
this.scheme = scheme;
this.skipTLSVerify = skipTLSVerify;
this.shouldPass = shouldPass;
this.motivation = motivation;
}

@Test
public void testCreateKubernetesAuthConfig() throws URISyntaxException {
try {
HttpClientWithTLSOptionsFactory.getBuilder(new URI(scheme, "localhost", null, null), null, skipTLSVerify);
if (!shouldPass) {
fail("This test was expected to fail, reason: " + motivation);
}
} catch (IllegalArgumentException e) {
if (shouldPass) {
fail("This test was expected to pass, reason: " + motivation);
}
} catch (HttpClientWithTLSOptionsFactory.TLSConfigurationError e) {
fail("This test should not cause a TLSConfigurationError");
}
}
}
Loading