Skip to content

[7.x] Secure password for monitoring HTTP exporter #51775

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 3 commits into from
Feb 3, 2020
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
18 changes: 12 additions & 6 deletions docs/reference/settings/monitoring-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,11 @@ For more information, see <<monitor-elasticsearch-cluster>>.
==== General Monitoring Settings

`xpack.monitoring.enabled`::
Set to `true` (default) to enable {es} {monitoring} for {es} on the node.
Set to `true` (default) to enable {es} {monitoring} for {es} on the node.
+
--
NOTE: To enable data collection, you must also set `xpack.monitoring.collection.enabled`
to `true`. Its default value is `false`.
NOTE: To enable data collection, you must also set `xpack.monitoring.collection.enabled`
to `true`. Its default value is `false`.
--

[float]
Expand All @@ -51,7 +51,7 @@ this setting is `false` (default), {es} monitoring data is not collected and
all monitoring data from other sources such as {kib}, Beats, and Logstash is
ignored.

`xpack.monitoring.collection.interval` (<<cluster-update-settings,Dynamic>>)::
`xpack.monitoring.collection.interval` (<<cluster-update-settings,Dynamic>>)::

Setting to `-1` to disable data collection is no longer supported beginning with
7.0.0. deprecated[6.3.0, Use `xpack.monitoring.collection.enabled` set to `false` instead.]
Expand Down Expand Up @@ -198,11 +198,17 @@ xpack.monitoring.exporters:

`auth.username`::

The username is required if a `auth.password` is supplied.
The username is required if `auth.secure_password` or `auth.password` is supplied.

`auth.secure_password` (<<secure-settings,Secure>>, <<reloadable-secure-settings,reloadable>>)::

The password for the `auth.username`. Takes precedence over `auth.password` if it is also specified.

`auth.password`::

The password for the `auth.username`.
The password for the `auth.username`. If `auth.secure_password` is also specified, this setting is ignored.

deprecated[7.7.0, Use `auth.secure_password` instead.]

`connection.timeout`::

Expand Down
1 change: 1 addition & 0 deletions docs/reference/setup/secure-settings.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,4 @@ There are reloadable secure settings for:
* {plugins}/discovery-ec2-usage.html#_configuring_ec2_discovery[The EC2 discovery plugin]
* {plugins}/repository-gcs-client.html[The GCS repository plugin]
* {plugins}/repository-s3-client.html[The S3 repository plugin]
* <<monitoring-settings>>
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,13 @@ public MockRequest takeRequest() {
return requests.poll();
}

/**
* Removes all requests from the queue.
*/
public void clearRequests() {
requests.clear();
}

/**
* A utility method to peek into the requests and find out if #MockWebServer.takeRequests will not throw an out of bound exception
* @return true if more requests are available, false otherwise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.elasticsearch.license.XPackLicenseState;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ReloadablePlugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptService;
Expand Down Expand Up @@ -73,7 +74,7 @@
* - node clients: all modules are bound
* - transport clients: only action/transport actions are bound
*/
public class Monitoring extends Plugin implements ActionPlugin {
public class Monitoring extends Plugin implements ActionPlugin, ReloadablePlugin {

/**
* The ability to automatically cleanup ".watcher_history*" indices while also cleaning up Monitoring indices.
Expand All @@ -86,6 +87,8 @@ public class Monitoring extends Plugin implements ActionPlugin {
private final boolean enabled;
private final boolean transportClientMode;

private Exporters exporters;

public Monitoring(Settings settings) {
this.settings = settings;
this.transportClientMode = XPackPlugin.transportClientMode(settings);
Expand Down Expand Up @@ -134,8 +137,7 @@ public Collection<Object> createComponents(Client client, ClusterService cluster
Map<String, Exporter.Factory> exporterFactories = new HashMap<>();
exporterFactories.put(HttpExporter.TYPE, config -> new HttpExporter(config, dynamicSSLService, threadPool.getThreadContext()));
exporterFactories.put(LocalExporter.TYPE, config -> new LocalExporter(config, client, cleanerService));
final Exporters exporters = new Exporters(settings, exporterFactories, clusterService, getLicenseState(),
threadPool.getThreadContext());
exporters = new Exporters(settings, exporterFactories, clusterService, getLicenseState(), threadPool.getThreadContext());

Set<Collector> collectors = new HashSet<>();
collectors.add(new IndexStatsCollector(clusterService, getLicenseState(), client));
Expand Down Expand Up @@ -196,4 +198,13 @@ public List<String> getSettingsFilter() {
final String exportersKey = "xpack.monitoring.exporters.";
return Collections.unmodifiableList(Arrays.asList(exportersKey + "*.auth.*", exportersKey + "*.ssl.*"));
}

@Override
public void reload(Settings settings) throws Exception {
final List<String> changedExporters = HttpExporter.loadSettings(settings);
for (String changedExporter : changedExporters) {
final Settings settingsForChangedExporter = settings.filter(x -> x.startsWith("xpack.monitoring.exporters." + changedExporter));
exporters.setExportersSetting(settingsForChangedExporter);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc;
import org.elasticsearch.xpack.monitoring.collector.Collector;
import org.elasticsearch.xpack.monitoring.exporter.Exporters;
import org.elasticsearch.xpack.monitoring.exporter.http.HttpExporter;

import java.io.Closeable;
import java.util.ArrayList;
Expand Down Expand Up @@ -104,6 +105,8 @@ public class MonitoringService extends AbstractLifecycleComponent {
.addSettingsUpdateConsumer(ELASTICSEARCH_COLLECTION_ENABLED, this::setElasticsearchCollectionEnabled);
clusterService.getClusterSettings().addSettingsUpdateConsumer(ENABLED, this::setMonitoringActive);
clusterService.getClusterSettings().addSettingsUpdateConsumer(INTERVAL, this::setInterval);

HttpExporter.loadSettings(settings);
}

void setElasticsearchCollectionEnabled(final boolean enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import static java.util.Collections.emptyMap;

Expand All @@ -58,15 +59,17 @@ public Exporters(Settings settings, Map<String, Exporter.Factory> factories,
this.clusterService = Objects.requireNonNull(clusterService);
this.licenseState = Objects.requireNonNull(licenseState);

clusterService.getClusterSettings().addSettingsUpdateConsumer(this::setExportersSetting, getSettings());
final List<Setting.AffixSetting<?>> dynamicSettings =
getSettings().stream().filter(Setting::isDynamic).collect(Collectors.toList());
clusterService.getClusterSettings().addSettingsUpdateConsumer(this::setExportersSetting, dynamicSettings);
HttpExporter.registerSettingValidators(clusterService);
// this ensures, that logging is happening by adding an empty consumer per affix setting
for (Setting.AffixSetting<?> affixSetting : getSettings()) {
// this ensures that logging is happening by adding an empty consumer per affix setting
for (Setting.AffixSetting<?> affixSetting : dynamicSettings) {
clusterService.getClusterSettings().addAffixUpdateConsumer(affixSetting, (s, o) -> {}, (s, o) -> {});
}
}

private void setExportersSetting(Settings exportersSetting) {
public void setExportersSetting(Settings exportersSetting) {
if (this.lifecycle.started()) {
Map<String, Exporter> updated = initExporters(exportersSetting);
closeExporters(logger, this.exporters.getAndSet(updated));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.collect.MapBuilder;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Setting.Property;
import org.elasticsearch.common.settings.Settings;
Expand All @@ -52,6 +54,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import java.util.function.Supplier;
Expand Down Expand Up @@ -205,23 +208,20 @@ public void validate(final String username, final Map<Setting<?>, Object> settin
final String namespace =
HttpExporter.AUTH_USERNAME_SETTING.getNamespace(
HttpExporter.AUTH_USERNAME_SETTING.getConcreteSetting(key));
final String password =
(String) settings.get(AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace));

// password must be specified along with username for any auth
if (Strings.isNullOrEmpty(username) == false) {
if (Strings.isNullOrEmpty(password)) {
throw new SettingsException(
"[" + AUTH_USERNAME_SETTING.getConcreteSettingForNamespace(namespace).getKey() + "] is set " +
"but [" + AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace).getKey() + "] is " +
"missing");
}
final String type =
(String) settings.get(Exporter.TYPE_SETTING.getConcreteSettingForNamespace(namespace));
if ("http".equals(type) == false) {
throw new SettingsException("username for [" + key + "] is set but type is [" + type + "]");
}
}

// it would be ideal to validate that just one of either AUTH_PASSWORD_SETTING or
// AUTH_SECURE_PASSWORD_SETTING were present here, but that is not currently possible with the settings
// validation framework.
// https://github.com/elastic/elasticsearch/issues/51332
}

@Override
Expand All @@ -231,8 +231,8 @@ public Iterator<Setting<?>> settings() {
HttpExporter.AUTH_USERNAME_SETTING.getConcreteSetting(key));

final List<Setting<?>> settings = Arrays.asList(
Exporter.TYPE_SETTING.getConcreteSettingForNamespace(namespace),
HttpExporter.AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace));
Exporter.TYPE_SETTING.getConcreteSettingForNamespace(namespace));

return settings.iterator();
}

Expand Down Expand Up @@ -284,8 +284,18 @@ public Iterator<Setting<?>> settings() {
},
Property.Dynamic,
Property.NodeScope,
Property.Filtered),
Property.Filtered,
Property.Deprecated),
TYPE_DEPENDENCY);
/**
* Secure password for basic auth.
*/
public static final Setting.AffixSetting<SecureString> AUTH_SECURE_PASSWORD_SETTING =
Setting.affixKeySetting(
"xpack.monitoring.exporters.",
"auth.secure_password",
key -> SecureSetting.secureString(key, null),
TYPE_DEPENDENCY);
/**
* The SSL settings.
*
Expand Down Expand Up @@ -400,6 +410,7 @@ public Iterator<Setting<?>> settings() {
*/
private final AtomicBoolean clusterAlertsAllowed = new AtomicBoolean(false);

private static final ConcurrentHashMap<String, SecureString> SECURE_AUTH_PASSWORDS = new ConcurrentHashMap<>();
private final ThreadContext threadContext;
private final DateFormatter dateTimeFormatter;

Expand Down Expand Up @@ -697,6 +708,25 @@ private static void configureTimeouts(final RestClientBuilder builder, final Con
builder.setRequestConfigCallback(new TimeoutRequestConfigCallback(connectTimeout, socketTimeout));
}


/**
* Caches secure settings for use when dynamically configuring HTTP exporters
* @param settings settings used for configuring HTTP exporter
* @return names of HTTP exporters whose secure settings changed, if any
*/
public static List<String> loadSettings(Settings settings) {
final List<String> changedExporters = new ArrayList<>();
for (final String namespace : AUTH_SECURE_PASSWORD_SETTING.getNamespaces(settings)) {
final Setting<SecureString> s = AUTH_SECURE_PASSWORD_SETTING.getConcreteSettingForNamespace(namespace);
final SecureString securePassword = s.get(settings);
final SecureString existingPassword = SECURE_AUTH_PASSWORDS.put(namespace, securePassword);
if (securePassword.equals(existingPassword) == false) {
changedExporters.add(namespace);
}
}
return changedExporters;
}

/**
* Creates the optional {@link CredentialsProvider} with the username/password to use with <em>all</em> requests for user
* authentication.
Expand All @@ -708,7 +738,19 @@ private static void configureTimeouts(final RestClientBuilder builder, final Con
@Nullable
private static CredentialsProvider createCredentialsProvider(final Config config) {
final String username = AUTH_USERNAME_SETTING.getConcreteSettingForNamespace(config.name()).get(config.settings());
final String password = AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(config.name()).get(config.settings());

final String deprecatedPassword = AUTH_PASSWORD_SETTING.getConcreteSettingForNamespace(config.name()).get(config.settings());
final SecureString securePassword = SECURE_AUTH_PASSWORDS.get(config.name());
final String password;
if (securePassword != null) {
password = securePassword.toString();
if (Strings.isNullOrEmpty(deprecatedPassword) == false) {
logger.warn("exporter [{}] specified both auth.secure_password and auth.password. using auth.secure_password and " +
"ignoring auth.password", config.name());
}
} else {
password = deprecatedPassword;
}

final CredentialsProvider credentialsProvider = new BasicCredentialsProvider();
credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(username, password));
Expand Down Expand Up @@ -873,9 +915,19 @@ public void doClose() {
}
}

public static List<Setting.AffixSetting<?>> getSettings() {
public static List<Setting.AffixSetting<?>> getDynamicSettings() {
return Arrays.asList(HOST_SETTING, TEMPLATE_CREATE_LEGACY_VERSIONS_SETTING, AUTH_PASSWORD_SETTING, AUTH_USERNAME_SETTING,
BULK_TIMEOUT_SETTING, CONNECTION_READ_TIMEOUT_SETTING, CONNECTION_TIMEOUT_SETTING, PIPELINE_CHECK_TIMEOUT_SETTING,
PROXY_BASE_PATH_SETTING, SNIFF_ENABLED_SETTING, TEMPLATE_CHECK_TIMEOUT_SETTING, SSL_SETTING, HEADERS_SETTING);
}

public static List<Setting.AffixSetting<?>> getSecureSettings() {
return Collections.singletonList(AUTH_SECURE_PASSWORD_SETTING);
}

public static List<Setting.AffixSetting<?>> getSettings() {
List<Setting.AffixSetting<?>> allSettings = new ArrayList<>(getDynamicSettings());
allSettings.addAll(getSecureSettings());
return allSettings;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,13 @@

public class LocalStateMonitoring extends LocalStateCompositeXPackPlugin {

final Monitoring monitoring;

public LocalStateMonitoring(final Settings settings, final Path configPath) throws Exception {
super(settings, configPath);
LocalStateMonitoring thisVar = this;

plugins.add(new Monitoring(settings) {
monitoring = new Monitoring(settings) {
@Override
protected SSLService getSslService() {
return thisVar.getSslService();
Expand All @@ -36,7 +38,8 @@ protected LicenseService getLicenseService() {
protected XPackLicenseState getLicenseState() {
return thisVar.getLicenseState();
}
});
};
plugins.add(monitoring);
plugins.add(new Watcher(settings) {
@Override
protected SSLService getSslService() {
Expand All @@ -50,4 +53,9 @@ protected XPackLicenseState getLicenseState() {
});
plugins.add(new IndexLifecycle(settings));
}

public Monitoring getMonitoring() {
return monitoring;
}

}
Loading