All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.elasticsearch.common.settings.LocallyMountedSecrets Maven / Gradle / Ivy

/*
 * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
 * or more contributor license agreements. Licensed under the Elastic License
 * 2.0 and the Server Side Public License, v 1; you may not use this file except
 * in compliance with, at your election, the Elastic License 2.0 or the Server
 * Side Public License, v 1.
 */

package org.elasticsearch.common.settings;

import org.apache.lucene.util.SetOnce;
import org.elasticsearch.TransportVersion;
import org.elasticsearch.Version;
import org.elasticsearch.common.hash.MessageDigests;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.env.Environment;
import org.elasticsearch.reservedstate.service.ReservedStateVersion;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.DeprecationHandler;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentParserConfiguration;

import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.GeneralSecurityException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.xcontent.XContentType.JSON;

/**
 * An implementation of {@link SecureSettings} which loads the secrets from
 * externally mounted local directory. It looks for the folder called 'secrets'
 * under the config directory. All secure settings should be supplied in a single
 * file called 'secrets.json' which sits inside the 'secrets' directory.
 * 

* If the 'secrets' directory or the 'secrets.json' file don't exist, the * SecureSettings implementation is loaded with empty settings map. *

* Example secrets.json format: * { * "metadata": { * "version": "1", * "compatibility": "8.7.0" * }, * "string_secrets": { * "secure.setting.key.one": "aaa", * "secure.setting.key.two": "bbb" * } * "file_secrets": { * "secure.setting.key.three": "Y2Nj" * } * } */ public class LocallyMountedSecrets implements SecureSettings { public static final String SECRETS_FILE_NAME = "secrets.json"; public static final String SECRETS_DIRECTORY = "secrets"; // TODO[wrb]: remove deprecated name once controller and performance have updated their formats public static final ParseField STRING_SECRETS_FIELD = new ParseField("string_secrets", "secrets"); public static final ParseField FILE_SECRETS_FIELD = new ParseField("file_secrets"); public static final ParseField METADATA_FIELD = new ParseField("metadata"); @SuppressWarnings("unchecked") private final ConstructingObjectParser secretsParser = new ConstructingObjectParser<>( "locally_mounted_secrets", a -> { final var decoder = Base64.getDecoder(); Map stringSecretsMap = a[0] == null ? Map.of() : ((Map) a[0]).entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().getBytes(StandardCharsets.UTF_8))); Map fileSecretsByteMap = a[1] == null ? Map.of() : ((Map) a[1]).entrySet() .stream() .collect(Collectors.toMap(Map.Entry::getKey, e -> decoder.decode(e.getValue()))); Set duplicateKeys = fileSecretsByteMap.keySet() .stream() .filter(stringSecretsMap::containsKey) .collect(Collectors.toSet()); if (duplicateKeys.isEmpty() == false) { throw new IllegalStateException("Some settings were defined as both string and file settings: " + duplicateKeys); } Map allSecrets = Stream.concat(stringSecretsMap.entrySet().stream(), fileSecretsByteMap.entrySet().stream()) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); return new LocalFileSecrets(allSecrets, (ReservedStateVersion) a[2]); } ); private final String secretsDir; private final String secretsFile; private final SetOnce secrets = new SetOnce<>(); /** * Direct constructor to be used by the CLI */ public LocallyMountedSecrets(Environment environment) { var secretsDirPath = resolveSecretsDir(environment); var secretsFilePath = resolveSecretsFile(environment); secretsParser.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> p.map(), STRING_SECRETS_FIELD); secretsParser.declareObject(ConstructingObjectParser.optionalConstructorArg(), (p, c) -> p.map(), FILE_SECRETS_FIELD); secretsParser.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> ReservedStateVersion.parse(p), METADATA_FIELD); if (Files.exists(secretsDirPath) && Files.exists(secretsFilePath)) { try { secrets.set(processSecretsFile(secretsFilePath)); } catch (IOException e) { throw new IllegalStateException("Error processing secrets file", e); } } else { secrets.set(new LocalFileSecrets(Map.of(), new ReservedStateVersion(-1L, Version.CURRENT))); } this.secretsDir = secretsDirPath.toString(); this.secretsFile = secretsFilePath.toString(); } /** * Resolve a secrets directory path given an environment * @param environment Elasticsearch environment * @return Secrets directory within an Elasticsearch environment */ public static Path resolveSecretsDir(Environment environment) { return environment.configFile().toAbsolutePath().resolve(SECRETS_DIRECTORY); } /** * Resolve a secure settings file path given an environment * @param environment Elasticsearch environment * @return Secure settings file within an Elasticsearch environment */ public static Path resolveSecretsFile(Environment environment) { return resolveSecretsDir(environment).resolve(SECRETS_FILE_NAME); } /** * Used by {@link org.elasticsearch.bootstrap.ServerArgs} to deserialize the secrets * when they are received by the Elasticsearch process. The ServerCli code serializes * the secrets as part of ServerArgs. */ public LocallyMountedSecrets(StreamInput in) throws IOException { this.secretsDir = in.readString(); this.secretsFile = in.readString(); if (in.readBoolean()) { secrets.set(LocalFileSecrets.readFrom(in)); } // TODO: Add support for watching for file changes here. } @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(secretsDir); out.writeString(secretsFile); if (secrets.get() == null) { out.writeBoolean(false); } else { out.writeBoolean(true); secrets.get().writeTo(out); } } @Override public boolean isLoaded() { return secrets.get() != null; } @Override public Set getSettingNames() { assert isLoaded(); return secrets.get().entries().keySet(); } @Override public SecureString getString(String setting) { assert isLoaded(); var value = secrets.get().entries().get(setting); if (value == null) { return null; } ByteBuffer byteBuffer = ByteBuffer.wrap(value); CharBuffer charBuffer = StandardCharsets.UTF_8.decode(byteBuffer); return new SecureString(Arrays.copyOfRange(charBuffer.array(), charBuffer.position(), charBuffer.limit())); } @Override public InputStream getFile(String setting) throws GeneralSecurityException { assert isLoaded(); return new ByteArrayInputStream(secrets.get().entries().get(setting)); } @Override public byte[] getSHA256Digest(String setting) throws GeneralSecurityException { assert isLoaded(); return MessageDigests.sha256().digest(secrets.get().entries().get(setting)); } /** * Returns version number from the secrets file */ public long getVersion() { return secrets.get().metadata.version(); } @Override public void close() throws IOException { if (null != secrets.get() && secrets.get().entries().isEmpty() == false) { for (var entry : secrets.get().entries().entrySet()) { entry.setValue(null); } } } // package private for testing LocalFileSecrets processSecretsFile(Path path) throws IOException { try ( var fis = Files.newInputStream(path); var bis = new BufferedInputStream(fis); var parser = JSON.xContent() .createParser(XContentParserConfiguration.EMPTY.withDeprecationHandler(DeprecationHandler.IGNORE_DEPRECATIONS), bis) ) { return secretsParser.apply(parser, null); } } record LocalFileSecrets(Map entries, ReservedStateVersion metadata) implements Writeable { /** * Read LocalFileSecrets from stream input * *

This class should only be used node-locally, to represent the local secrets on a particular * node. Thus, the transport version should always be {@link TransportVersion#current()} */ public static LocalFileSecrets readFrom(StreamInput in) throws IOException { assert in.getTransportVersion() == TransportVersion.current(); return new LocalFileSecrets(in.readMap(StreamInput::readByteArray), ReservedStateVersion.readFrom(in)); } /** * Write LocalFileSecrets to stream output * *

This class should only be used node-locally, to represent the local secrets on a particular * node. Thus, the transport version should always be {@link TransportVersion#current()} */ @Override public void writeTo(StreamOutput out) throws IOException { assert out.getTransportVersion() == TransportVersion.current(); out.writeMap((entries == null) ? Map.of() : entries, StreamOutput::writeString, StreamOutput::writeByteArray); metadata.writeTo(out); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy