Support mTLS for gRPC channel (#15)

This commit is contained in:
Daming 2021-08-31 15:56:30 +08:00 committed by GitHub
parent d47f9cc370
commit dd0d8afe88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 172 additions and 17 deletions

View File

@ -14,6 +14,7 @@ Release Notes.
* Fix kafka-reporter-plugin shade package conflict
* Add all config items to `agent.conf` file for convenient containerization use cases.
* Advanced Kafka Producer configuration enhancement.
* Suport mTLS for gRPC channel.
#### Documentation

View File

@ -135,6 +135,21 @@ public class Config {
* Force open TLS for gRPC channel if true.
*/
public static boolean FORCE_TLS = false;
/**
* SSL trusted ca file. If it exists, will enable TLS for gRPC channel.
*/
public static String SSL_TRUSTED_CA_PATH = "ca" + Constants.PATH_SEPARATOR + "ca.crt";
/**
* Key cert chain file. If ssl_cert_chain and ssl_key exist, will enable mTLS for gRPC channel.
*/
public static String SSL_CERT_CHAIN_PATH;
/**
* Private key file. If ssl_cert_chain and ssl_key exist, will enable mTLS for gRPC channel.
*/
public static String SSL_KEY_PATH;
}
public static class OsInfo {

View File

@ -23,31 +23,54 @@ import io.grpc.netty.NegotiationType;
import io.grpc.netty.NettyChannelBuilder;
import io.netty.handler.ssl.SslContextBuilder;
import java.io.File;
import javax.net.ssl.SSLException;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.skywalking.apm.agent.core.boot.AgentPackageNotFoundException;
import org.apache.skywalking.apm.agent.core.boot.AgentPackagePath;
import org.apache.skywalking.apm.agent.core.conf.Config;
import org.apache.skywalking.apm.agent.core.conf.Constants;
import org.apache.skywalking.apm.agent.core.logging.api.ILog;
import org.apache.skywalking.apm.agent.core.logging.api.LogManager;
import org.apache.skywalking.apm.agent.core.util.PrivateKeyUtil;
import org.apache.skywalking.apm.util.StringUtil;
/**
* Detect the `/ca` folder in agent package, if `ca.crt` exists, start TLS (no mutual auth).
* If only ca.crt exists, start TLS. If cert, key and ca files exist, enable mTLS.
*/
public class TLSChannelBuilder implements ChannelBuilder<NettyChannelBuilder> {
private static String CA_FILE_NAME = "ca" + Constants.PATH_SEPARATOR + "ca.crt";
private static final ILog LOGGER = LogManager.getLogger(TLSChannelBuilder.class);
@Override
public NettyChannelBuilder build(
NettyChannelBuilder managedChannelBuilder) throws AgentPackageNotFoundException, SSLException {
File caFile = new File(AgentPackagePath.getPath(), CA_FILE_NAME);
NettyChannelBuilder managedChannelBuilder) throws AgentPackageNotFoundException, IOException {
File caFile = new File(AgentPackagePath.getPath(), Config.Agent.SSL_TRUSTED_CA_PATH);
boolean isCAFileExist = caFile.exists() && caFile.isFile();
if (Config.Agent.FORCE_TLS || isCAFileExist) {
SslContextBuilder builder = GrpcSslContexts.forClient();
if (isCAFileExist) {
String certPath = Config.Agent.SSL_CERT_CHAIN_PATH;
String keyPath = Config.Agent.SSL_KEY_PATH;
if (StringUtil.isNotBlank(certPath) && StringUtil.isNotBlank(keyPath)) {
File keyFile = new File(AgentPackagePath.getPath(), keyPath);
File certFile = new File(AgentPackagePath.getPath(), certPath);
if (certFile.isFile() && keyFile.isFile()) {
try (InputStream cert = new FileInputStream(certFile);
InputStream key = PrivateKeyUtil.loadDecryptionKey(keyPath)) {
builder.keyManager(cert, key);
}
} else if (!certFile.isFile() || !keyFile.isFile()) {
LOGGER.warn("Failed to enable mTLS caused by cert or key cannot be found.");
}
}
builder.trustManager(caFile);
}
managedChannelBuilder = managedChannelBuilder.negotiationType(NegotiationType.TLS)
.sslContext(builder.build());
managedChannelBuilder.negotiationType(NegotiationType.TLS).sslContext(builder.build());
}
return managedChannelBuilder;
}
}

View File

@ -0,0 +1,80 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.skywalking.apm.agent.core.util;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
/**
* Util intends to parse PKCS#1 and PKCS#8 at same time.
*/
public class PrivateKeyUtil {
private static final String PKCS_1_PEM_HEADER = "-----BEGIN RSA PRIVATE KEY-----";
private static final String PKCS_1_PEM_FOOTER = "-----END RSA PRIVATE KEY-----";
private static final String PKCS_8_PEM_HEADER = "-----BEGIN PRIVATE KEY-----";
private static final String PKCS_8_PEM_FOOTER = "-----END PRIVATE KEY-----";
/**
* Load a RSA decryption key from a file (PEM or DER).
*/
public static InputStream loadDecryptionKey(String keyFilePath) throws IOException {
byte[] keyDataBytes = Files.readAllBytes(Paths.get(keyFilePath));
String keyDataString = new String(keyDataBytes, StandardCharsets.UTF_8);
if (keyDataString.contains(PKCS_1_PEM_HEADER)) {
// OpenSSL / PKCS#1 Base64 PEM encoded file
keyDataString = keyDataString.replace(PKCS_1_PEM_HEADER, "");
keyDataString = keyDataString.replace(PKCS_1_PEM_FOOTER, "");
keyDataString = keyDataString.replace("\n", "");
return readPkcs1PrivateKey(Base64.getDecoder().decode(keyDataString));
}
return new ByteArrayInputStream(keyDataString.getBytes());
}
/**
* Create a InputStream instance from raw PKCS#1 bytes. Raw Java API can't recognize ASN.1 format, so we should
* convert it into a pkcs#8 format Java can understand.
*/
private static InputStream readPkcs1PrivateKey(byte[] pkcs1Bytes) {
int pkcs1Length = pkcs1Bytes.length;
int totalLength = pkcs1Length + 22;
byte[] pkcs8Header = new byte[] {
0x30, (byte) 0x82, (byte) ((totalLength >> 8) & 0xff), (byte) (totalLength & 0xff), // Sequence + total length
0x2, 0x1, 0x0, // Integer (0)
0x30, 0xD, 0x6, 0x9, 0x2A, (byte) 0x86, 0x48, (byte) 0x86, (byte) 0xF7, 0xD, 0x1, 0x1, 0x1, 0x5, 0x0, // Sequence: 1.2.840.113549.1.1.1, NULL
0x4, (byte) 0x82, (byte) ((pkcs1Length >> 8) & 0xff), (byte) (pkcs1Length & 0xff) // Octet string + length
};
StringBuilder pkcs8 = new StringBuilder(PKCS_8_PEM_HEADER);
pkcs8.append("\n").append(new String(Base64.getEncoder().encode(join(pkcs8Header, pkcs1Bytes))));
pkcs8.append("\n").append(PKCS_8_PEM_FOOTER);
return new ByteArrayInputStream(pkcs8.toString().getBytes());
}
private static byte[] join(byte[] byteArray1, byte[] byteArray2) {
byte[] bytes = new byte[byteArray1.length + byteArray2.length];
System.arraycopy(byteArray1, 0, bytes, 0, byteArray1.length);
System.arraycopy(byteArray2, 0, bytes, byteArray1.length, byteArray2.length);
return bytes;
}
}

View File

@ -68,6 +68,14 @@ agent.keep_tracing=${SW_AGENT_KEEP_TRACING:false}
# If true, SkyWalking agent uses TLS even no CA file detected.
agent.force_tls=${SW_AGENT_FORCE_TLS:false}
# gRPC SSL trusted ca file.
agent.ssl_trusted_ca_path=${SW_AGENT_SSL_TRUSTED_CA_PATH:/ca/ca.crt}
# enable mTLS when ssl_key_path and ssl_cert_chain_path exist.
agent.ssl_key_path=${SW_AGENT_SSL_KEY_PATH:}
agent.ssl_cert_chain_path=${SW_AGENT_SSL_CERT_CHAIN_PATH:}
# Limit the length of the ipv4 list size.
osinfo.ipv4_list_size=${SW_AGENT_OSINFO_IPV4_LIST_SIZE:10}

View File

@ -7,20 +7,45 @@ at the same time, the SkyWalking backend is in another region (VPC).
>
> Because of that, security requirement is very obvious.
## Creating SSL/TLS Certificates
The first step is to generate certificates and key files for encrypting communication. This is
fairly straightforward: use `openssl` from the command line.
Use this [script](../../../../../tools/TLS/tls_key_generate.sh) if you are not familiar with how to generate key files.
We need the following files:
- `client.pem`: A private RSA key to sign and authenticate the public key. It's either a PKCS#8(PEM) or PKCS#1(DER).
- `client.crt`: Self-signed X.509 public keys for distribution.
- `ca.crt`: A certificate authority public key for a client to validate the server's certificate.
## Authentication Mode
Only support **no mutual auth**.
- Use this [script](../../../../../tools/TLS/tls_key_generate.sh) if you are not familiar with how to generate key files.
- Find `ca.crt`, and use it at client side
- Find `server.crt` ,`server.pem` and `ca.crt`. Use them at server side. Please refer to `gRPC SSL` of the OAP server doc.
for more details.
- Find `ca.crt`, and use it at client side. In `mTLS` mode, `client.crt` and `client.pem` are required at client side.
- Find `server.crt`, `server.pem` and `ca.crt`. Use them at server side. Please refer to `gRPC Security` of the OAP server doc for more details.
## Open and config TLS
### Agent config
- Place `ca.crt` into `/ca` folder in agent package. Notice, `/ca` is not created in distribution, please create it by yourself.
- Agent open TLS automatically after the `/ca/ca.crt` file detected.
- Agent enables TLS automatically after the `ca.crt`(by default `/ca` folder in agent package) file is detected.
- TLS with no CA mode could be activated by this setting.
```
agent.force_tls=${SW_AGENT_FORCE_TLS:false}
agent.force_tls=${SW_AGENT_FORCE_TLS:true}
```
## Enable mutual TLS
- Sharing gRPC server must be started with mTLS enabled. More details can be found in `receiver-sharing-server` section in `application.yaml`. Please refer to `gRPC Security` and `gRPC/HTTP server for receiver`.
- Copy CA certificate, certificate and private key of client into `agent/ca`.
- Configure client-side SSL/TLS in `agent.conf`.
- Change `SW_AGENT_COLLECTOR_BACKEND_SERVICES` targeting to host and port of `receiver-sharing-server`.
For example:
```
agent.force_tls=${SW_AGENT_FORCE_TLS:true}
agent.ssl_trusted_ca_path=${SW_AGENT_SSL_TRUSTED_CA_PATH:/ca/ca.crt}
agent.ssl_key_path=${SW_AGENT_SSL_KEY_PATH:/ca/client.pem}
agent.ssl_cert_chain_path=${SW_AGENT_SSL_CERT_CHAIN_PATH:/ca/client.crt}
collector.backend_service=${SW_AGENT_COLLECTOR_BACKEND_SERVICES:skywalking-oap:11801}
```
Notice, the client-side's certificate and the private key are from the same CA certificate with server-side.

View File

@ -20,6 +20,9 @@ property key | Description | Default |
`agent.operation_name_threshold `|The operationName max length, setting this value > 190 is not recommended.|`150`|
`agent.keep_tracing`|Keep tracing even the backend is not available if this value is `true`.|`false`|
`agent.force_tls`|Force open TLS for gRPC channel if this value is `true`.|`false`|
`agent.ssl_trusted_ca_path` | gRPC SSL trusted ca file. | `/ca/ca.crt` |
`agent.ssl_key_path`| The private key file. Enable mTLS when ssl_key_path and ssl_cert_chain_path exist. | `""` |
`agent.ssl_cert_chain_path`| The certificate file. Enable mTLS when ssl_key_path and ssl_cert_chain_path exist. | `""` |
`osinfo.ipv4_list_size`| Limit the length of the ipv4 list size. |`10`|
`collector.grpc_channel_check_interval`|grpc channel status check interval.|`30`|
`collector.heartbeat_period`|agent heartbeat report period. Unit, second.|`30`|