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

allow user config CA and tls #33

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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: 1 addition & 1 deletion doc/splunk-devops-usage.md
Original file line number Diff line number Diff line change
@@ -147,7 +147,7 @@ Example: `java -Dsplunkins.buffer=4096 -jar jenkins.war`
|splunkins.debugLogBatchSize|128|batch size for sending verbose level (FINE,FINER,FINEST) log record|
|splunkins.enableRemoteTaskListenerDecorator|true|whether to capture remote TaskListener|
|splunkins.ignoreConfigChangePattern|(?:queue|nodeMonitors|UpdateCenter|global-build-stats).xml|/(?:fingerprint|builds|config-history)/.*?xml|regular expression for ignoring config file changes|
|splunkins.verifySSL|false|enable ssl certificate verification for splunk endpoint|
|splunkins.verifySSL|false|enable ssl certificate verification for splunk endpoint,deprecated, use form config instead|
|splunkins.junitStdioLimit|2097152|trim long junit standard output/error, set it to 0 for unlimited|
|splunkins.decodePipelineConsole|true|decode pipeline job console note to get parallel label, link href|
|splunkins.auditPostRequest|true|audit trail for script invoking, credentials updating|
3 changes: 3 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -73,6 +73,9 @@
<id>splunk-artifactory</id>
<name>Splunk Releases</name>
<url>https://splunk.jfrog.io/splunk/ext-releases-local</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@

import com.splunk.splunkjenkins.model.EventType;
import com.splunk.splunkjenkins.model.MetaDataConfigItem;
import com.splunk.splunkjenkins.utils.CustomSSLConnectionSocketFactory;
import com.splunk.splunkjenkins.utils.SplunkLogService;
import groovy.lang.GroovyCodeSource;
import hudson.Extension;
@@ -26,27 +27,16 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.net.*;
import java.security.cert.CertificateException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import static com.splunk.splunkjenkins.Constants.*;
import static com.splunk.splunkjenkins.utils.LogEventHelper.getDefaultDslScript;
import static com.splunk.splunkjenkins.utils.LogEventHelper.nonEmpty;
import static com.splunk.splunkjenkins.utils.LogEventHelper.validateGroovyScript;
import static com.splunk.splunkjenkins.utils.LogEventHelper.verifyHttpInput;
import static com.splunk.splunkjenkins.utils.LogEventHelper.*;
import static groovy.lang.GroovyShell.DEFAULT_CODE_BASE;
import static java.util.regex.Pattern.CASE_INSENSITIVE;
import static org.apache.commons.lang.StringUtils.isEmpty;
@@ -80,6 +70,8 @@ public class SplunkJenkinsInstallation extends GlobalConfiguration {
private String metadataSource;
private String ignoredJobs;
private Boolean globalPipelineFilter;
private Boolean verifyCertificate;
private String customCA;

//below are all transient properties
public transient Properties metaDataProperties = new Properties();
@@ -166,6 +158,7 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
this.metadataItemSet = null; // otherwise bindJSON will never clear it once set
boolean previousState = this.enabled;
String previousCAConfig = this.customCA + "|" + this.verifyCertificate;
req.bindJSON(this, formData);
if (this.metadataItemSet == null) {
this.metaDataConfig = "";
@@ -178,6 +171,14 @@ public boolean configure(StaplerRequest req, JSONObject formData) throws FormExc
}
updateCache();
save();
String currentCAConfig = this.customCA + "|" + this.verifyCertificate;
// updated ca cert or tls verify flag
if (this.useSSL && this.enabled && !currentCAConfig.endsWith(previousCAConfig)) {
LOG.warning("tls config changed, need rebuild connection pool");
this.enabled = false;
SplunkLogService.getInstance().rebuild(this.verifyCertificate, this.customCA);
this.enabled = true;
}
if (previousState && !this.enabled) {
//switch from enable to disable
SplunkLogService.getInstance().stopWorker();
@@ -223,7 +224,9 @@ public FormValidation doCheckToken(@QueryParameter("value") String value) {
@RequirePOST
public FormValidation doTestHttpInput(@QueryParameter String host, @QueryParameter int port,
@QueryParameter String token, @QueryParameter boolean useSSL,
@QueryParameter String metaDataConfig) {
@QueryParameter String metaDataConfig, @QueryParameter boolean verifyCertificate,
@QueryParameter String customCA
) {
Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
//create new instance to avoid pollution global config
SplunkJenkinsInstallation config = new SplunkJenkinsInstallation(false);
@@ -233,6 +236,8 @@ public FormValidation doTestHttpInput(@QueryParameter String host, @QueryParamet
config.useSSL = useSSL;
config.metaDataConfig = metaDataConfig;
config.enabled = true;
config.verifyCertificate = verifyCertificate;
config.customCA = customCA;
config.updateCache();
if (!config.isValid()) {
return FormValidation.error(Messages.InvalidHostOrToken());
@@ -268,6 +273,18 @@ public FormValidation doCheckIgnoredJobs(@QueryParameter String value) {
return FormValidation.ok();
}

@RequirePOST
public FormValidation doCheckCustomCA(@QueryParameter("value") String value) {
if (StringUtils.isNotBlank(value)) {
try {
CustomSSLConnectionSocketFactory.textToX509Cert(value);
} catch (CertificateException e) {
return FormValidation.error("can not parse certificate");
}
}
return FormValidation.ok();
}

////////END OF FORM VALIDATION/////////
protected void updateCache() {
if (!this.enabled) {
@@ -534,6 +551,8 @@ public Map toMap() {
map.put("retriesOnError", retriesOnError);
map.put("metadataHost", metadataHost);
map.put("metadataSource", metadataSource);
map.put("verifyCertificate", verifyCertificate);
map.put("customCA", customCA);
return map;
}

@@ -645,6 +664,10 @@ private void migrate() {
if (this.globalPipelineFilter == null) {
this.globalPipelineFilter = true;
}
if (this.verifyCertificate == null) {
// previous version doesn't have such field
this.verifyCertificate = Boolean.getBoolean("splunkins.verifySSL");
}
}

public String getIgnoredJobs() {
@@ -666,4 +689,24 @@ public void setGlobalPipelineFilter(Boolean globalPipelineFilter) {
public boolean isPipelineFilterEnabled() {
return Boolean.TRUE.equals(globalPipelineFilter);
}

public Boolean getVerifyCertificate() {
return verifyCertificate;
}

public boolean isTlsVerify() {
return Boolean.TRUE.equals(verifyCertificate);
}

public void setVerifyCertificate(Boolean verifyCertificate) {
this.verifyCertificate = verifyCertificate;
}

public String getCustomCA() {
return customCA;
}

public void setCustomCA(String customCA) {
this.customCA = customCA;
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,95 @@
package com.splunk.splunkjenkins.utils;

import org.apache.commons.lang.StringUtils;
import shaded.splk.org.apache.http.HttpHost;
import shaded.splk.org.apache.http.conn.ssl.NoopHostnameVerifier;
import shaded.splk.org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import shaded.splk.org.apache.http.conn.ssl.TrustStrategy;
import shaded.splk.org.apache.http.protocol.HttpContext;
import shaded.splk.org.apache.http.ssl.SSLContexts;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.logging.Level;
import static java.nio.charset.StandardCharsets.UTF_8;

import static com.splunk.splunkjenkins.utils.MultipleHostResolver.NAME_DELIMITER;

public class CustomSSLConnectionSocketFactory extends SSLConnectionSocketFactory {
public static final java.util.logging.Logger LOG = java.util.logging.Logger.getLogger(CustomSSLConnectionSocketFactory.class.getName());

public static SSLConnectionSocketFactory getSocketFactory(boolean verifyCA, String certificate) {
if (!verifyCA) {
SSLContext sslContext = null;
try {
TrustStrategy acceptingTrustStrategy = new TrustAllStrategy();
sslContext = SSLContexts.custom().loadTrustMaterial(null, acceptingTrustStrategy).build();
} catch (Exception e) {
LOG.log(Level.WARNING, "init custom ssl context with TrustAllStrategy failed", e);
sslContext = SSLContexts.createDefault();
}
return new CustomSSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
}
// tls verify is on
SSLContext sslContext;
if (StringUtils.isNotBlank(certificate)) {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
X509Certificate customCA = textToX509Cert(certificate);
trustStore.setCertificateEntry("splunk-http-events-ca", customCA);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
TrustManager[] trustManagers = tmf.getTrustManagers();
sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustManagers, null);
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException | KeyManagementException ex) {
LOG.log(Level.WARNING, "init custom ssl context failed", ex);
//invalid CA or keystore error
sslContext = SSLContexts.createDefault();
}
} else {
sslContext = SSLContexts.createDefault();
}
return new CustomSSLConnectionSocketFactory(sslContext, SSLConnectionSocketFactory.getDefaultHostnameVerifier());
}

public CustomSSLConnectionSocketFactory(SSLContext sslContext, HostnameVerifier hostnameVerifier) {
super(sslContext, hostnameVerifier);
super(sslContext, new String[]{"TLSv1.3", "TLSv1.2"}, null, hostnameVerifier);
}

@Override
public Socket connectSocket(int connectTimeout, Socket socket, HttpHost host, InetSocketAddress remoteAddress, InetSocketAddress localAddress, HttpContext context) throws IOException {
if (host.getHostName().contains(NAME_DELIMITER)) {
HttpHost resolvedHost = new HttpHost(remoteAddress.getHostName(), host.getPort(), host.getSchemeName());
return super.connectSocket(connectTimeout, socket, resolvedHost, remoteAddress, localAddress, context);
} else{
} else {
return super.connectSocket(connectTimeout, socket, host, remoteAddress, localAddress, context);
}
}

public static X509Certificate textToX509Cert(String permCert) throws CertificateException {
X509Certificate certificate = (X509Certificate) CertificateFactory.getInstance("X509").generateCertificate(new ByteArrayInputStream(permCert.getBytes(UTF_8)));
return certificate;
}

static class TrustAllStrategy implements TrustStrategy {
public boolean isTrusted(X509Certificate[] certificate,
String type) {
return true;
}
}
}
Original file line number Diff line number Diff line change
@@ -35,10 +35,14 @@
import org.apache.commons.io.IOUtils;
import shaded.splk.org.apache.http.HttpResponse;
import shaded.splk.org.apache.http.client.HttpClient;
import shaded.splk.org.apache.http.client.config.CookieSpecs;
import shaded.splk.org.apache.http.client.config.RequestConfig;
import shaded.splk.org.apache.http.client.entity.GzipCompressingEntity;
import shaded.splk.org.apache.http.client.methods.HttpPost;
import shaded.splk.org.apache.http.client.utils.URIBuilder;
import shaded.splk.org.apache.http.conn.HttpClientConnectionManager;
import shaded.splk.org.apache.http.entity.StringEntity;
import shaded.splk.org.apache.http.impl.client.HttpClients;
import shaded.splk.org.apache.http.util.EntityUtils;

import java.io.*;
@@ -138,7 +142,10 @@ private static void updateContent(HttpPost postMethod, String message, boolean i

public static FormValidation verifyHttpInput(SplunkJenkinsInstallation config) {
HttpPost post = buildPost(new EventRecord("ping from jenkins plugin", EventType.LOG), config);
HttpClient client = SplunkLogService.getInstance().getClient();
HttpClientConnectionManager mgr=SplunkLogService.getInstance().buildConnectionManager(config.isTlsVerify(),config.getCustomCA());
HttpClient client=HttpClients.custom().setConnectionManager(mgr).useSystemProperties()
.setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build())
.build();
try {
HttpResponse response = client.execute(post);
if (response.getStatusLine().getStatusCode() != 200) {
@@ -165,6 +172,7 @@ public static FormValidation verifyHttpInput(SplunkJenkinsInstallation config) {
return FormValidation.error(e.getMessage());
} finally {
post.releaseConnection();
mgr.shutdown();
}
return FormValidation.ok("Splunk connection verified");
}
Loading