solution: allow TLS configuration for proxy

This commit is contained in:
Igor Artamonov
2020-03-20 20:22:01 -04:00
parent 26f8dd294e
commit d4bd180e23
25 changed files with 882 additions and 72 deletions

View File

@@ -34,7 +34,8 @@ class ProxyStarter(
@Autowired private val env: Environment,
@Autowired private val readRpcJson: ReadRpcJson,
@Autowired private val writeRpcJson: WriteRpcJson,
@Autowired private val nativeCall: NativeCall
@Autowired private val nativeCall: NativeCall,
@Autowired private val tlsSetup: TlsSetup
) {
companion object {
@@ -48,7 +49,7 @@ class ProxyStarter(
log.debug("Proxy server is not configured")
return
}
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall)
val server = ProxyServer(config, readRpcJson, writeRpcJson, nativeCall, tlsSetup)
server.start()
}

View File

@@ -0,0 +1,83 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle
import io.emeraldpay.dshackle.config.AuthConfig
import io.grpc.netty.GrpcSslContexts
import io.netty.handler.ssl.ClientAuth
import io.netty.handler.ssl.SslContext
import io.netty.handler.ssl.SslContextBuilder
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.stereotype.Service
@Service
class TlsSetup(
@Autowired val fileResolver: FileResolver
) {
companion object {
private val log = LoggerFactory.getLogger(TlsSetup::class.java)
}
fun setupServer(category: String, config: AuthConfig.ServerTlsAuth): SslContext? {
val mustBeSecure = config.enabled != null && config.enabled!!
val tlsDisabled = config.enabled != null && !config.enabled!!
var hasServerCertificate = true
if (!tlsDisabled) {
if (StringUtils.isEmpty(config.certificate)) {
if (mustBeSecure) {
log.error("tls.server.certificate property for $category is not set (path to server TLS certificate) but TLS is enabled")
throw IllegalArgumentException("Certificate not set")
}
hasServerCertificate = false
}
if (StringUtils.isEmpty(config.key)) {
if (mustBeSecure) {
log.error("tls.server.key property for $category is not set (path to server TLS certificate key) but TLS is enabled")
throw IllegalArgumentException("Certificate Key not set")
}
hasServerCertificate = false
}
}
if (mustBeSecure || (!tlsDisabled && hasServerCertificate)) {
log.info("Using TLS for $category")
val sslContextBuilder = SslContextBuilder.forServer(
fileResolver.resolve(config.certificate!!),
fileResolver.resolve(config.key!!)
)
if (StringUtils.isNotEmpty(config.clientCa)) {
log.info("Using TLS for client authentication for $category")
sslContextBuilder.trustManager(
fileResolver.resolve(config.clientCa!!)
)
if (config.clientRequire != null && config.clientRequire!!) {
sslContextBuilder.clientAuth(ClientAuth.REQUIRE)
}
} else if (config.clientRequire != null && config.clientRequire!!) {
throw IllegalArgumentException("Client Certificate not set")
} else {
log.warn("Trust all clients for $category")
}
return sslContextBuilder.build()
} else {
log.warn("Using insecure transport for $category")
}
return null
}
}

View File

@@ -0,0 +1,60 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import org.slf4j.LoggerFactory
class AuthConfig {
companion object {
private val log = LoggerFactory.getLogger(AuthConfig::class.java)
}
open class ClientAuth {
var type: String? = null
}
class ClientBasicAuth(
val username: String,
val password: String
) : ClientAuth()
class ClientTlsAuth(
var ca: String? = null,
var certificate: String? = null,
var key: String? = null
) : ClientAuth()
/**
* Example config:
* ```
* enabled: false
* server:
* certificate: "127.0.0.1.crt"
* key: "127.0.0.1.p8.key"
* client:
* require: false
* ca: "ca.dshackle.test.crt"
* ```
*/
open class ServerTlsAuth {
var enabled: Boolean? = null
var certificate: String? = null
var key: String? = null
var clientRequire: Boolean? = null
var clientCa: String? = null
}
}

View File

@@ -0,0 +1,82 @@
/**
* Copyright (c) 2020 ETCDEV GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.emeraldpay.dshackle.config
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.nodes.MappingNode
class AuthConfigReader : YamlConfigReader() {
companion object {
private val log = LoggerFactory.getLogger(AuthConfigReader::class.java)
}
fun readClientBasicAuth(node: MappingNode?): AuthConfig.ClientBasicAuth? {
return getMapping(node, "basic-auth")?.let { authNode ->
val username = getValueAsString(authNode, "username")
val password = getValueAsString(authNode, "password")
if (username != null && password != null) {
AuthConfig.ClientBasicAuth(username, password)
} else {
log.warn("Basic auth is not fully configured")
null
}
}
}
fun readClientTls(node: MappingNode?): AuthConfig.ClientTlsAuth? {
return getMapping(node, "tls")?.let { authNode ->
val auth = AuthConfig.ClientTlsAuth()
auth.ca = getValueAsString(authNode, "ca")
auth.certificate = getValueAsString(authNode, "certificate")
auth.key = getValueAsString(authNode, "key")
auth
}
}
/**
* Example config:
* ```
* enabled: false
* server:
* certificate: "127.0.0.1.crt"
* key: "127.0.0.1.p8.key"
* client:
* require: false
* ca: "ca.dshackle.test.crt"
* ```
*/
fun readServerTls(node: MappingNode?): AuthConfig.ServerTlsAuth? {
return getMapping(node, "tls")?.let { node ->
val auth = AuthConfig.ServerTlsAuth()
getValueAsBool(node, "enabled")?.let {
auth.enabled = it
}
getMapping(node, "server")?.let { node ->
auth.certificate = getValueAsString(node, "certificate")
auth.key = getValueAsString(node, "key")
}
getMapping(node, "client")?.let { node ->
getValueAsBool(node, "require")?.let {
auth.clientRequire = it
}
auth.clientCa = getValueAsString(node, "ca")
}
auth
}
}
}

View File

@@ -38,6 +38,11 @@ class ProxyConfig {
*/
var port: Int = 8080
/**
* TLS Auth required from clients.
*/
var tls: AuthConfig.ServerTlsAuth? = null
/**
* List of available routes
*/

View File

@@ -33,10 +33,10 @@ class ProxyConfigReader : YamlConfigReader() {
}
private var filename = "dshackle.yaml"
private val authConfigReader = AuthConfigReader()
fun read(input: InputStream): ProxyConfig? {
val yaml = Yaml()
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
val configNode = readNode(input)
return read(getMapping(configNode, "proxy"))
}
@@ -73,8 +73,10 @@ class ProxyConfigReader : YamlConfigReader() {
}
}
if (config.routes.isEmpty()) {
log.warn("Proxy config has no routes")
return null
}
config.tls = authConfigReader.readServerTls(input)
return config
}

View File

@@ -17,7 +17,6 @@ package io.emeraldpay.dshackle.config
import io.emeraldpay.dshackle.Defaults
import java.net.URI
import java.time.Duration
import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
@@ -81,7 +80,7 @@ class UpstreamsConfig {
class GrpcConnection : UpstreamConnection() {
var host: String? = null
var port: Int = 0
var auth: TlsAuth? = null
var auth: AuthConfig.ClientTlsAuth? = null
}
class EthereumConnection : UpstreamConnection() {
@@ -90,29 +89,16 @@ class UpstreamsConfig {
}
class HttpEndpoint(val url: URI) {
var basicAuth: BasicAuth? = null
var tls: TlsAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
var tls: AuthConfig.ClientTlsAuth? = null
}
class WsEndpoint(val url: URI) {
var origin: URI? = null
var basicAuth: BasicAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
}
open class Auth {
var type: String? = null
}
class BasicAuth(
val username: String,
val password: String
) : Auth()
class TlsAuth(
var ca: String? = null,
var certificate: String? = null,
var key: String? = null
) : Auth()
//TODO make it unmodifiable after initial load
class Labels: HashMap<String, String>() {

View File

@@ -18,24 +18,21 @@ package io.emeraldpay.dshackle.config
import org.apache.commons.lang3.StringUtils
import org.slf4j.LoggerFactory
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.ScalarNode
import reactor.util.function.Tuples
import java.io.InputStream
import java.io.InputStreamReader
import java.lang.IllegalArgumentException
import java.net.URI
import java.time.Duration
class UpstreamsConfigReader : YamlConfigReader() {
private val log = LoggerFactory.getLogger(UpstreamsConfigReader::class.java)
private val authConfigReader = AuthConfigReader()
fun read(input: InputStream): UpstreamsConfig {
val yaml = Yaml()
val configNode = asMappingNode(yaml.compose(InputStreamReader(input)))
val configNode = readNode(input)
val config = UpstreamsConfig()
config.version = getValueAsString(configNode, "version")
@@ -65,8 +62,8 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsString(node, "url")?.let { url ->
val http = UpstreamsConfig.HttpEndpoint(URI(url))
connection.rpc = http
http.basicAuth = readBasicAuth(node)
http.tls = readTls(node)
http.basicAuth = authConfigReader.readClientBasicAuth(node)
http.tls = authConfigReader.readClientTls(node)
}
}
getMapping(connConfigNode, "ws")?.let { node ->
@@ -76,7 +73,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsString(node, "origin")?.let { origin ->
ws.origin = URI(origin)
}
ws.basicAuth = readBasicAuth(node)
ws.basicAuth = authConfigReader.readClientBasicAuth(node)
}
}
} else {
@@ -97,7 +94,7 @@ class UpstreamsConfigReader : YamlConfigReader() {
getValueAsInt(connConfigNode, "port")?.let {
connection.port = it
}
connection.auth = readTls(connConfigNode)
connection.auth = authConfigReader.readClientTls(connConfigNode)
} else {
log.error("Upstream at #0 has invalid configuration")
}
@@ -194,27 +191,4 @@ class UpstreamsConfigReader : YamlConfigReader() {
return options
}
private fun readBasicAuth(node: MappingNode?): UpstreamsConfig.BasicAuth? {
return getMapping(node, "basic-auth")?.let { authNode ->
val username = getValueAsString(authNode, "username")
val password = getValueAsString(authNode, "password")
if (username != null && password != null) {
UpstreamsConfig.BasicAuth(username, password)
} else {
log.warn("Basic auth is not fully configured")
null
}
}
}
private fun readTls(node: MappingNode?): UpstreamsConfig.TlsAuth? {
return getMapping(node, "tls")?.let { authNode ->
val auth = UpstreamsConfig.TlsAuth()
auth.ca = getValueAsString(authNode, "ca")
auth.certificate = getValueAsString(authNode, "certificate")
auth.key = getValueAsString(authNode, "key")
auth
}
}
}

View File

@@ -16,14 +16,26 @@
package io.emeraldpay.dshackle.config
import io.emeraldpay.grpc.Chain
import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.nodes.CollectionNode
import org.yaml.snakeyaml.nodes.MappingNode
import org.yaml.snakeyaml.nodes.Node
import org.yaml.snakeyaml.nodes.ScalarNode
import java.io.InputStream
import java.io.InputStreamReader
open class YamlConfigReader {
private val envVariables = EnvVariables()
fun readNode(input: String): MappingNode {
return readNode(input.byteInputStream())
}
fun readNode(input: InputStream): MappingNode {
val yaml = Yaml()
return asMappingNode(yaml.compose(InputStreamReader(input)))
}
protected fun hasAny(mappingNode: MappingNode?, key: String): Boolean {
if (mappingNode == null) {
return false

View File

@@ -17,9 +17,11 @@ package io.emeraldpay.dshackle.proxy
import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.Common
import io.emeraldpay.dshackle.TlsSetup
import io.emeraldpay.dshackle.config.ProxyConfig
import io.emeraldpay.dshackle.rpc.NativeCall
import io.netty.buffer.Unpooled
import io.netty.handler.ssl.SslContextBuilder
import org.reactivestreams.Publisher
import org.slf4j.LoggerFactory
import org.springframework.http.HttpHeaders
@@ -29,6 +31,7 @@ import reactor.netty.http.server.HttpServer
import reactor.netty.http.server.HttpServerRequest
import reactor.netty.http.server.HttpServerResponse
import reactor.netty.http.server.HttpServerRoutes
import java.io.File
import java.util.function.BiFunction
/**
@@ -38,7 +41,8 @@ class ProxyServer(
private var config: ProxyConfig,
private val readRpcJson: ReadRpcJson,
private val writeRpcJson: WriteRpcJson,
private val nativeCall: NativeCall
private val nativeCall: NativeCall,
private val tlsSetup: TlsSetup
) {
companion object {
@@ -51,9 +55,17 @@ class ProxyServer(
return
}
log.info("Listening Proxy on ${config.host}:${config.port}")
val server: DisposableServer = HttpServer.create()
var serverBuilder = HttpServer.create()
.host(config.host)
.port(config.port)
config.tls?.let { tls ->
tlsSetup.setupServer("proxy", tls)?.let { sslContext ->
serverBuilder = serverBuilder.secure { secure -> secure.sslContext(sslContext) }
}
}
val server: DisposableServer = serverBuilder
.route(this::setupRoutes)
.bindNow()
}

View File

@@ -18,6 +18,7 @@ package io.emeraldpay.dshackle.upstream.ethereum
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.cache.Caches
import io.emeraldpay.dshackle.cache.CachesEnabled
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.reader.EmptyReader
import io.emeraldpay.dshackle.reader.Reader
@@ -45,7 +46,7 @@ class EthereumWs(
.builder<BlockJson<TransactionRefJson>>()
.name("new-blocks")
.build()
var basicAuth: UpstreamsConfig.BasicAuth? = null
var basicAuth: AuthConfig.ClientBasicAuth? = null
private var blockCache: Reader<BlockHash, BlockJson<TransactionRefJson>> = EmptyReader()

View File

@@ -20,6 +20,7 @@ import io.emeraldpay.api.proto.BlockchainOuterClass
import io.emeraldpay.api.proto.ReactorBlockchainGrpc
import io.emeraldpay.dshackle.Defaults
import io.emeraldpay.dshackle.FileResolver
import io.emeraldpay.dshackle.config.AuthConfig
import io.emeraldpay.dshackle.config.UpstreamsConfig
import io.emeraldpay.dshackle.upstream.UpstreamAvailability
import io.emeraldpay.dshackle.startup.UpstreamChange
@@ -45,7 +46,7 @@ class GrpcUpstreams(
private val host: String,
private val port: Int,
private val objectMapper: ObjectMapper,
private val auth: UpstreamsConfig.TlsAuth? = null,
private val auth: AuthConfig.ClientTlsAuth? = null,
private val fileResolver: FileResolver
) {
private val log = LoggerFactory.getLogger(GrpcUpstreams::class.java)
@@ -133,7 +134,7 @@ class GrpcUpstreams(
return Flux.fromIterable(removed + added)
}
internal fun withTls(auth: UpstreamsConfig.TlsAuth): SslContext {
internal fun withTls(auth: AuthConfig.ClientTlsAuth): SslContext {
val sslContext = SslContextBuilder.forClient()
.clientAuth(ClientAuth.REQUIRE)
sslContext.trustManager(fileResolver.resolve(auth.ca!!).inputStream())