Skip to content

Commit

Permalink
Add OpAMP connection settings capability to the example
Browse files Browse the repository at this point in the history
This change demonstrates OpAMP Connection Setting Offer Flow
from OpAMP spec (see https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#opamp-connection-setting-offer-flow)

- The example agent and server now use TLS for OpAMP connection.
- The server's certificate is signed by a CA authority. The CA
  authority's certificate is self-generated (can be re-generated
  using examples/certs/generate.sh).
- The server may generate and offer a client-side certificate to the agent.
  The generated certificate is signed by the same CA.
- The client can accept the certificate and use it for future
  connections.
- Server UI now allows the user to generate a certificate to
  offer to the agent (for the first time or to rotate). The flows
  are described in the OpAMP spec, see
  https://github.com/open-telemetry/opamp-spec/blob/main/specification.md#trust-on-first-use

 WARNING: all included certificate files are examples only and MUST NOT
 be used in production.

 How to test:
 1. Run the server.
 2. Run the agent.
 3. Go to http://localhost:4321/
 4. Find the connected agent in the list and click it
 5. Scroll down and click "Accept and Offer Client Certificate" button.
  • Loading branch information
tigrannajaryan committed Jun 21, 2023
1 parent 8968fb4 commit 54300ea
Show file tree
Hide file tree
Showing 22 changed files with 811 additions and 24 deletions.
109 changes: 103 additions & 6 deletions internal/examples/agent/agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ package agent

import (
"context"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"log"
"math/rand"
"os"
"runtime"
Expand Down Expand Up @@ -55,6 +59,10 @@ type Agent struct {
remoteConfigStatus *protobufs.RemoteConfigStatus

metricReporter *MetricReporter

// The TLS certificate used for the OpAMP connection. Can be nil, meaning no client-side
// certificate is used.
opampClientCert *tls.Certificate
}

func NewAgent(logger types.Logger, agentType string, agentVersion string) *Agent {
Expand All @@ -70,19 +78,20 @@ func NewAgent(logger types.Logger, agentType string, agentVersion string) *Agent
agent.instanceId.String(), agentType, agentVersion)

agent.loadLocalConfig()
if err := agent.start(); err != nil {
agent.logger.Errorf("Cannot start OpAMP client: %v", err)
if err := agent.connect(); err != nil {
agent.logger.Errorf("Cannot connect OpAMP client: %v", err)
return nil
}

return agent
}

func (agent *Agent) start() error {
func (agent *Agent) connect() error {
agent.opampClient = client.NewWebSocket(agent.logger)

settings := types.StartSettings{
OpAMPServerURL: "ws://127.0.0.1:4320/v1/opamp",
OpAMPServerURL: "wss://127.0.0.1:4320/v1/opamp",
TLSConfig: createClientTLSConfig(agent.opampClientCert),
InstanceUid: agent.instanceId.String(),
Callbacks: types.CallbacksStruct{
OnConnectFunc: func() {
Expand All @@ -100,14 +109,17 @@ func (agent *Agent) start() error {
GetEffectiveConfigFunc: func(ctx context.Context) (*protobufs.EffectiveConfig, error) {
return agent.composeEffectiveConfig(), nil
},
OnMessageFunc: agent.onMessage,
OnMessageFunc: agent.onMessage,
OnOpampConnectionSettingsFunc: agent.onOpampConnectionSettings,
},
RemoteConfigStatus: agent.remoteConfigStatus,
Capabilities: protobufs.AgentCapabilities_AgentCapabilities_AcceptsRemoteConfig |
protobufs.AgentCapabilities_AgentCapabilities_ReportsRemoteConfig |
protobufs.AgentCapabilities_AgentCapabilities_ReportsEffectiveConfig |
protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnMetrics,
protobufs.AgentCapabilities_AgentCapabilities_ReportsOwnMetrics |
protobufs.AgentCapabilities_AgentCapabilities_AcceptsOpAMPConnectionSettings,
}

err := agent.opampClient.SetAgentDescription(agent.agentDescription)
if err != nil {
return err
Expand All @@ -125,6 +137,34 @@ func (agent *Agent) start() error {
return nil
}

func createClientTLSConfig(clientCert *tls.Certificate) *tls.Config {
// Read the CA's public key. This is the CA that signs the server's certificate.
caCertBytes, err := os.ReadFile("../certs/certs/ca.cert.pem")
if err != nil {
log.Fatalln(err)
}

// Create a certificate pool and make our CA trusted.
caCertPool := x509.NewCertPool()
if ok := caCertPool.AppendCertsFromPEM(caCertBytes); !ok {
log.Fatalln("Cannot append ca.cert.pem")
}

cfg := &tls.Config{
RootCAs: caCertPool,
}
if clientCert != nil {
// If there is a client-side certificate use it for connection too.
cfg.Certificates = []tls.Certificate{*clientCert}
}
return cfg
}

func (agent *Agent) disconnect() {
agent.logger.Debugf("Disconnecting from server...")
agent.opampClient.Stop(context.Background())
}

func (agent *Agent) createAgentIdentity() {
// Generate instance id.
entropy := ulid.Monotonic(rand.New(rand.NewSource(0)), 0)
Expand Down Expand Up @@ -354,3 +394,60 @@ func (agent *Agent) onMessage(ctx context.Context, msg *types.MessageData) {
}
}
}

func (agent *Agent) onOpampConnectionSettings(ctx context.Context, settings *protobufs.OpAMPConnectionSettings) error {
if settings == nil || settings.Certificate == nil {
agent.logger.Debugf("Received nil certificate offer, ignoring.\n")
}

cert, err := agent.getCertFromSettings(settings.Certificate)
if err != nil {
return err
}

agent.logger.Debugf("Reconnecting to verify offered client certificate.\n")

agent.disconnect()
agent.opampClientCert = cert
if err := agent.connect(); err != nil {
agent.logger.Errorf("Cannot connect using offered certificate: %s. Ignoring the offer\n", err)
agent.opampClientCert = nil

if err := agent.connect(); err != nil {
agent.logger.Errorf("Unable to reconnect after restoring client certificate: %v\n", err)
return err
}
}

agent.logger.Debugf("Successfully connected to server. Accepting new client certificate.\n")

// TODO: we can also persist the successfully accepted certificate and use it when the
// agent connects to the server after the restart.

return nil
}

func (agent *Agent) getCertFromSettings(certificate *protobufs.TLSCertificate) (*tls.Certificate, error) {
// Parse the key pair to a certificate that can be used for network connections.
cert, err := tls.X509KeyPair(
certificate.PublicKey,
certificate.PrivateKey,
)
if err != nil {
agent.logger.Errorf("Received invalid certificate offer: %s\n", err)
return nil, err
}

if len(certificate.CaPublicKey) != 0 {
caCertPB, _ := pem.Decode(certificate.CaPublicKey)
caCert, err := x509.ParseCertificate(caCertPB.Bytes)
if err != nil {
agent.logger.Errorf("Cannot parse CA cert: %v", err)
return nil, err
}
agent.logger.Debugf("Received offer signed by CA: %v", caCert.Subject)
// TODO: we can verify the CA's identity here (to match our CA as we know it).
}

return &cert, nil
}
31 changes: 31 additions & 0 deletions internal/examples/certs/certs/ca.cert.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
-----BEGIN CERTIFICATE-----
MIIFWDCCA0CgAwIBAgIJAP2SOjxtoybOMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV
BAYTAkNBMQswCQYDVQQIDAJPTjENMAsGA1UEBwwEQ2l0eTEWMBQGA1UECgwNT3BB
TVAgRXhhbXBsZTAeFw0yMzA2MTkxOTI5MzRaFw0zMzA2MTYxOTI5MzRaMEExCzAJ
BgNVBAYTAkNBMQswCQYDVQQIDAJPTjENMAsGA1UEBwwEQ2l0eTEWMBQGA1UECgwN
T3BBTVAgRXhhbXBsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANv2
DWQXgQXW9rWyBsARgE1QvdHhrTxd5582+pscPszD/y0LnR3qF7/AExTVANlC+YmN
ge7OTqT6q+Xmr+3zarEonsmmitcK1mRih2Qns9VkrtBI8Z4gFKsu+8sulepvmUjy
3atgTOZ9eJjI8MagMt++onvE/jE5QKzmSNURTz0bDOnAzDpKTNq5r3zaItlQZLuZ
bOEr35QyRpfrchFu58doPuFjCczn6GenIMWpuotvooYf3CFV6fbb7e1yZBEsqWrD
GJ7HPFvjaudG41Rc0mKTE67fD+OOhC6C2McKk+WYpZ5lJEIzNJy3GE4GU+ASVaOe
57uT8NJTHc1a4MPlGD0pJ8hkwYF2Uka7bRPNsiYZnMgKgLEtvV7dyjKDvlT6Y8J/
Upb+643o4UuZf/VawWHfkmsZ2yZKI7zO1kxmrOcfiPH7E+OmfAZ6fZwzhy/yJzfz
Y96E1jeKnBaZWJRI/g1LatpOgJPaFszs0zzCQeIGPFkK85TAPyV9uvZk7itpXWCO
vVPbNQxvqZGYNVeqEUk1MqQQOroF6k7/+9+5+9tzvEzVW26l50op4yX77wQ41t8b
1z9WRf++CJV+fMswpY8ZS/8eIVEmSfmqoy7DzdfX9n18ug5zrIhbcueFklhy7odf
QVBTVXFhg2ePGLX0aumJDccu3pj23AG5Ksoz9BD3AgMBAAGjUzBRMB0GA1UdDgQW
BBRMKb3rHoJVbI26EM8oMc5JDk+xkzAfBgNVHSMEGDAWgBRMKb3rHoJVbI26EM8o
Mc5JDk+xkzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4ICAQDIK7WL
Uz2O2gqf71JTd2BXXQXmABTqFAcTNLADMYNiTG1REzFIRAeEiKaoVlEhtGgXV97E
Rkipt1wiUwykWlt9uk+oxK5asaNjtXmaQh9Miov5srFDYdJSfGiipCfjPSQXX761
zahBXlJ1QchOYdv1AndH1JxUmJifHY8Vk3SOyOemRZml+2PCW4U+B0ZEiEu3lXDE
5WURQtgWBzDAjQRTKeSGuxt0kyC0brlp+nFEzh8FgjV/5TiItrp11XFXC41No79t
HIQraJqxSVQ3roiuvUobUZQR+F7X51Hxe4iY4gebWREqR7489r37pcmSrf6bqY9y
NMgyXzc6eKU2pl1qSl4MdsvBE+a1Lc34xi6jzOQpJlpVSHSYzBHYmD6ddkRWAUrR
usZmkmK/9BbmjxAWOPDdkWQkIOv0+kaiPSlzWKnQfMMiCvVCwdN3VYf/XaLiZMnj
cy+ESm7KTBzxS6DKkgBMIV8PS8RvwHxYdB+3YK73aeZPOgBq9RB7+ohjjQZgOrNQ
QIMlfHoKqFCoHLYvU7wIcyspfj/bc+xOcVs18vwQoaH7Uqt/XPwOrZ4lkWv4bSeE
xfpfH5Q5/+QaBatYbzf5eu0iShXOSc6+Rf2cgfu4P7KQuo5zw1e5gvYdUbv7Ycel
e6n/SQk5TAClnSWYqwwxig3XSZLBNkxLqYnetA==
-----END CERTIFICATE-----
8 changes: 8 additions & 0 deletions internal/examples/certs/clear.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
rm certs/*
rm client_certs/*
rm server_certs/*
rm private/*
rm index.*
rm serial.*
echo 01 > serial
touch index.txt
22 changes: 22 additions & 0 deletions internal/examples/certs/client.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
[req]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no

[req_distinguished_name]
countryName = CA
stateOrProvinceName = ON
localityName = City
organizationName = OpAMP Example
commonName = OpAMP Example Client

[req_ext]
subjectAltName = @alt_names

[v3_req]
subjectAltName = @alt_names

[alt_names]
IP.1 = 127.0.0.1
35 changes: 35 additions & 0 deletions internal/examples/certs/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# First create a certificate authority (CA) that will sign all serve and client certificates.
# The server and the client trust this CA. Typically the public key of the CA certificate
# will be hard-coded in the server and client implementations (and never changes or changes
# only in the catastrophic even of CA private key leak or when predefined CA rotation time
# comes - typically annual or rarer).

# Create CA private key
openssl genrsa -out private/ca.key.pem 4096

# Create CA certificate
openssl req -new -x509 -days 3650 -key private/ca.key.pem -out certs/ca.cert.pem -config openssl.conf

# This section shows how to generate a client-side certificate that the agent can use
# to connect to the OpAMP server for the first time. This is not currently used in
# the example, but we show how it can be done if needed.
#
# Create a private key for client certificate.
# openssl genrsa -out client_certs/client.key.pem 4096
#
# Generate a client CRS
# openssl req -new -key client_certs/client.key.pem -out client_certs/client.csr -config client.conf
#
# Create a client certificate
# openssl ca -config openssl.conf -days 1650 -notext -batch -in client_certs/client.csr -out client_certs/client.cert.pem
# The generated pair of files in client_certs can be now used by TLS connection.

# Create private key for server certificate
openssl genrsa -out server_certs/server.key.pem 4096

# Generate server CRS
openssl req -new -key server_certs/server.key.pem -out server_certs/server.csr -config server.conf

# Create Server certificate
openssl ca -config openssl.conf -extfile server_ext.conf -days 1650 -notext -batch -in server_certs/server.csr -out server_certs/server.cert.pem
# The generated pair of files in server_certs can be now used by TLS connection.
1 change: 1 addition & 0 deletions internal/examples/certs/index.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
V 271225192934Z 01 unknown /C=CA/ST=ON/O=OpAMP Example/CN=127.0.0.1
1 change: 1 addition & 0 deletions internal/examples/certs/index.txt.attr
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unique_subject = yes
Empty file.
105 changes: 105 additions & 0 deletions internal/examples/certs/openssl.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# This definition stops the following lines choking if HOME isn't
# defined.
HOME = .
RANDFILE = $ENV::HOME/.rnd

# Extra OBJECT IDENTIFIER info:
#oid_file = $ENV::HOME/.oid
oid_section = new_oids

[ new_oids ]
# Policies used by the TSA examples.
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7

####################################################################
[ ca ]
default_ca = CA_default # The default ca section

[ CA_default ]
dir = ./ # Where everything is kept
certs = $dir/certs # Where the issued certs are kept
database = $dir/index.txt # database index file.
# several certs with same subject.
new_certs_dir = $dir/certs # default place for new certs.
certificate = $dir/certs/ca.cert.pem # The CA certificate
serial = $dir/serial # The current serial number
crlnumber = $dir/crlnumber # the current crl number
# must be commented out to leave a V1 CRL
private_key = $dir/private/ca.key.pem # The private key

name_opt = ca_default # Subject Name options
cert_opt = ca_default # Certificate field options

default_days = 365 # how long to certify for
default_crl_days= 30 # how long before next CRL
default_md = sha256 # use SHA-256 by default
preserve = no # keep passed DN ordering
policy = policy_match

# For the CA policy
[ policy_match ]
countryName = match
stateOrProvinceName = match
organizationName = match
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional

####################################################################
[ req ]
default_bits = 2048
default_md = sha256
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
attributes = req_attributes
x509_extensions = v3_ca # The extentions to add to the self signed cert

[ req_distinguished_name ]
countryName = Country Name (2 letter code)
countryName_default = CA
countryName_min = 2
countryName_max = 2
stateOrProvinceName = State or Province Name (full name)
stateOrProvinceName_default = ON
localityName = Locality Name (eg, city)
localityName_default = City
0.organizationName = Organization Name (eg, company)
0.organizationName_default = OpAMP Example
organizationalUnitName = Organizational Unit Name (eg, section)
commonName = Common Name (eg, your name or your server\'s hostname)
commonName_max = 64
emailAddress = Email Address
emailAddress_max = 64

[ req_attributes ]
challengePassword = A challenge password
challengePassword_min = 4
challengePassword_max = 20
unstructuredName = An optional company name


[ v3_req ]
# Extensions to add to a certificate request
basicConstraints = CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment

[ v3_ca ]
# Extensions for a typical CA
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = critical,CA:true

[ crl_ext ]
# issuerAltName=issuer:copy
authorityKeyIdentifier=keyid:always
Loading

0 comments on commit 54300ea

Please sign in to comment.