1// Copyright 2019 Istio Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15package sdscompare 16 17import ( 18 "crypto/x509" 19 "encoding/pem" 20 "fmt" 21 "time" 22 23 envoy_admin "github.com/envoyproxy/go-control-plane/envoy/admin/v3" 24 auth "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" 25 "github.com/golang/protobuf/ptypes" 26 27 "istio.io/istio/istioctl/pkg/util/configdump" 28 "istio.io/istio/security/pkg/nodeagent/sds" 29 "istio.io/pkg/log" 30) 31 32// SecretItemDiff represents a secret that has been diffed between nodeagent and proxy 33type SecretItemDiff struct { 34 Agent string `json:"agent"` 35 Proxy string `json:"proxy"` 36 SecretItem 37} 38 39// SecretItem is an intermediate representation of secrets, used to provide a common 40// format between the envoy proxy secrets and node agent output which can be diffed 41type SecretItem struct { 42 Name string `json:"resource_name"` 43 Data string `json:"cert"` 44 Source string `json:"source"` 45 Destination string `json:"destination"` 46 State string `json:"state"` 47 SecretMeta 48} 49 50// SecretMeta holds selected fields which can be extracted from parsed x509 cert 51type SecretMeta struct { 52 Valid bool `json:"cert_valid"` 53 SerialNumber string `json:"serial_number"` 54 NotAfter string `json:"not_after"` 55 NotBefore string `json:"not_before"` 56 Type string `json:"type"` 57} 58 59// NewSecretItemBuilder returns a new builder to create a secret item 60func NewSecretItemBuilder() SecretItemBuilder { 61 return &secretItemBuilder{} 62} 63 64// SecretItemBuilder wraps the process of setting fields for the SecretItem 65// and builds the Metadata fields from the cert contents behind the scenes 66type SecretItemBuilder interface { 67 Name(string) SecretItemBuilder 68 Data(string) SecretItemBuilder 69 Source(string) SecretItemBuilder 70 Destination(string) SecretItemBuilder 71 State(string) SecretItemBuilder 72 Build() (SecretItem, error) 73} 74 75// secretItemBuilder implements SecretItemBuilder, and acts as an intermediate before SecretItem generation 76type secretItemBuilder struct { 77 name string 78 data string 79 source string 80 dest string 81 state string 82 SecretMeta 83} 84 85// Name sets the name field on a secretItemBuilder 86func (s *secretItemBuilder) Name(name string) SecretItemBuilder { 87 s.name = name 88 return s 89} 90 91// Data sets the data field on a secretItemBuilder 92func (s *secretItemBuilder) Data(data string) SecretItemBuilder { 93 s.data = data 94 return s 95} 96 97// Source sets the source field on a secretItemBuilder 98func (s *secretItemBuilder) Source(source string) SecretItemBuilder { 99 s.source = source 100 return s 101} 102 103// Destination sets the destination field on a secretItemBuilder 104func (s *secretItemBuilder) Destination(dest string) SecretItemBuilder { 105 s.dest = dest 106 return s 107} 108 109// State sets the state of the secret on the agent or sidecar 110func (s *secretItemBuilder) State(state string) SecretItemBuilder { 111 s.state = state 112 return s 113} 114 115// Build takes the set fields from the builder and constructs the actual SecretItem 116// including generating the SecretMeta from the supplied cert data, if present 117func (s *secretItemBuilder) Build() (SecretItem, error) { 118 result := SecretItem{ 119 Name: s.name, 120 Data: s.data, 121 Source: s.source, 122 Destination: s.dest, 123 State: s.state, 124 } 125 126 var meta SecretMeta 127 var err error 128 if s.data != "" { 129 meta, err = secretMetaFromCert([]byte(s.data)) 130 if err != nil { 131 log.Debugf("failed to parse secret resource %s from source %s: %v", 132 s.name, s.source, err) 133 result.Valid = false 134 return result, nil 135 } 136 result.SecretMeta = meta 137 result.Valid = true 138 return result, nil 139 } 140 result.Valid = false 141 return result, nil 142} 143 144// connNameFilter used to provide a filter function through which node agent secrets can be filtered out 145type connNameFilter func(string) bool 146 147// GetNodeAgentSecrets takes the sds.Debug results provided to the comparator and parses them into []SecretItem 148func GetNodeAgentSecrets( 149 agentResponses map[string]sds.Debug, connFilter connNameFilter) ([]SecretItem, error) { 150 secrets := make([]SecretItem, 0) 151 for nodeAgentPod, debug := range agentResponses { 152 for _, client := range debug.Clients { 153 // note that the node agent contains secrets for all pods being served on that node 154 // we don't want to include the secret unless the pod name is included in the ProxyID 155 if connFilter(client.ProxyID) { 156 builder := NewSecretItemBuilder() 157 builder.Name(client.ResourceName).Source(nodeAgentPod).Destination(client.ProxyID) 158 if client.CertificateChain != "" { 159 builder.Data(client.CertificateChain) 160 } else if client.RootCert != "" { 161 builder.Data(client.RootCert) 162 } 163 164 secret, err := builder.Build() 165 if err != nil { 166 return nil, fmt.Errorf("error building node agent secret") 167 } 168 secrets = append(secrets, secret) 169 } 170 171 } 172 } 173 174 return secrets, nil 175} 176 177// GetEnvoySecrets parses the secrets section of the config dump into []SecretItem 178func GetEnvoySecrets( 179 wrapper *configdump.Wrapper) ([]SecretItem, error) { 180 secretConfigDump, err := wrapper.GetSecretConfigDump() 181 if err != nil { 182 return nil, err 183 } 184 185 proxySecretItems := make([]SecretItem, 0) 186 for _, warmingSecret := range secretConfigDump.DynamicWarmingSecrets { 187 secret, err := parseDynamicSecret(warmingSecret, "WARMING") 188 if err != nil { 189 return nil, fmt.Errorf("failed building warming secret %s: %v", 190 warmingSecret.Name, err) 191 } 192 proxySecretItems = append(proxySecretItems, secret) 193 } 194 for _, activeSecret := range secretConfigDump.DynamicActiveSecrets { 195 secret, err := parseDynamicSecret(activeSecret, "ACTIVE") 196 if err != nil { 197 return nil, fmt.Errorf("failed building warming secret %s: %v", 198 activeSecret.Name, err) 199 } 200 proxySecretItems = append(proxySecretItems, secret) 201 } 202 return proxySecretItems, nil 203} 204 205func parseDynamicSecret(s *envoy_admin.SecretsConfigDump_DynamicSecret, state string) (SecretItem, error) { 206 builder := NewSecretItemBuilder() 207 builder.Name(s.Name).State(state) 208 209 secretTyped := &auth.Secret{} 210 err := ptypes.UnmarshalAny(s.GetSecret(), secretTyped) 211 if err != nil { 212 return SecretItem{}, err 213 } 214 215 certChainSecret := secretTyped. 216 GetTlsCertificate(). 217 GetCertificateChain(). 218 GetInlineBytes() 219 caDataSecret := secretTyped. 220 GetValidationContext(). 221 GetTrustedCa(). 222 GetInlineBytes() 223 224 // seems as though the most straightforward way to tell whether this is a root ca or not 225 // is to check whether the inline bytes of the cert chain or the trusted ca field is zero length 226 if len(certChainSecret) > 0 { 227 builder.Data(string(certChainSecret)) 228 } else if len(caDataSecret) > 0 { 229 builder.Data(string(caDataSecret)) 230 } 231 232 secret, err := builder.Build() 233 if err != nil { 234 return SecretItem{}, fmt.Errorf("error building secret: %v", err) 235 } 236 237 return secret, nil 238} 239 240func secretMetaFromCert(rawCert []byte) (SecretMeta, error) { 241 block, _ := pem.Decode(rawCert) 242 if block == nil { 243 return SecretMeta{}, fmt.Errorf("failed to parse certificate PEM") 244 } 245 cert, err := x509.ParseCertificate(block.Bytes) 246 if err != nil { 247 return SecretMeta{}, err 248 } 249 var certType string 250 if cert.IsCA { 251 certType = "CA" 252 } else { 253 certType = "Cert Chain" 254 } 255 256 return SecretMeta{ 257 SerialNumber: fmt.Sprintf("%d", cert.SerialNumber), 258 NotAfter: cert.NotAfter.Format(time.RFC3339), 259 NotBefore: cert.NotBefore.Format(time.RFC3339), 260 Type: certType, 261 }, nil 262} 263