1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365
|
// Copyright 2015 Google Inc. All Rights Reserved.
//
// 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 certs implements a CertSource which speaks to the public Cloud SQL API endpoint.
package certs
import (
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"math"
mrand "math/rand"
"net/http"
"strings"
"sync"
"time"
"github.com/GoogleCloudPlatform/cloudsql-proxy/logging"
"github.com/GoogleCloudPlatform/cloudsql-proxy/proxy/util"
"golang.org/x/oauth2"
"google.golang.org/api/googleapi"
sqladmin "google.golang.org/api/sqladmin/v1beta4"
)
var defaultUserAgent = util.UserAgentFromVersionString()
// NewCertSource returns a CertSource which can be used to authenticate using
// the provided client, which must not be nil.
//
// This function is deprecated; use NewCertSourceOpts instead.
func NewCertSource(host string, c *http.Client, checkRegion bool) *RemoteCertSource {
return NewCertSourceOpts(c, RemoteOpts{
APIBasePath: host,
IgnoreRegion: !checkRegion,
UserAgent: defaultUserAgent,
})
}
// RemoteOpts are a collection of options for NewCertSourceOpts. All fields are
// optional.
type RemoteOpts struct {
// APIBasePath specifies the base path for the sqladmin API. If left blank,
// the default from the autogenerated sqladmin library is used (which is
// sufficient for nearly all users)
APIBasePath string
// IgnoreRegion specifies whether a missing or mismatched region in the
// instance name should be ignored. In a future version this value will be
// forced to 'false' by the RemoteCertSource.
IgnoreRegion bool
// A string for the RemoteCertSource to identify itself when contacting the
// sqladmin API.
UserAgent string
// IP address type options
IPAddrTypeOpts []string
// Enable IAM proxy db authentication
EnableIAMLogin bool
// Token source for token information used in cert creation
TokenSource oauth2.TokenSource
// DelayKeyGenerate, if true, causes the RSA key to be generated lazily
// on the first connection to a database. The default behavior is to generate
// the key when the CertSource is created.
DelayKeyGenerate bool
}
// NewCertSourceOpts returns a CertSource configured with the provided Opts.
// The provided http.Client must not be nil.
//
// Use this function instead of NewCertSource; it has a more forward-compatible
// signature.
func NewCertSourceOpts(c *http.Client, opts RemoteOpts) *RemoteCertSource {
serv, err := sqladmin.New(c)
if err != nil {
panic(err) // Only will happen if the provided client is nil.
}
if opts.APIBasePath != "" {
serv.BasePath = opts.APIBasePath
}
ua := opts.UserAgent
if ua == "" {
ua = defaultUserAgent
}
serv.UserAgent = ua
// Set default value to be "PUBLIC,PRIVATE" if not specified
if len(opts.IPAddrTypeOpts) == 0 {
opts.IPAddrTypeOpts = []string{"PUBLIC", "PRIVATE"}
}
// Add "PUBLIC" as an alias for "PRIMARY"
for index, ipAddressType := range opts.IPAddrTypeOpts {
if strings.ToUpper(ipAddressType) == "PUBLIC" {
opts.IPAddrTypeOpts[index] = "PRIMARY"
}
}
certSource := &RemoteCertSource{
serv: serv,
checkRegion: !opts.IgnoreRegion,
IPAddrTypes: opts.IPAddrTypeOpts,
EnableIAMLogin: opts.EnableIAMLogin,
TokenSource: opts.TokenSource,
}
if !opts.DelayKeyGenerate {
// Generate the RSA key now, but don't block on it.
go certSource.generateKey()
}
return certSource
}
// RemoteCertSource implements a CertSource, using Cloud SQL APIs to
// return Local certificates for identifying oneself as a specific user
// to the remote instance and Remote certificates for confirming the
// remote database's identity.
type RemoteCertSource struct {
// keyOnce is used to create `key` lazily.
keyOnce sync.Once
// key is the private key used for certificates returned by Local.
key *rsa.PrivateKey
// serv is used to make authenticated API calls to Cloud SQL.
serv *sqladmin.Service
// If set, providing an incorrect region in their connection string will be
// treated as an error. This is to provide the same functionality that will
// occur when API calls require the region.
checkRegion bool
// a list of ip address types that users select
IPAddrTypes []string
// flag to enable IAM proxy db authentication
EnableIAMLogin bool
// token source for the token information used in cert creation
TokenSource oauth2.TokenSource
}
// Constants for backoffAPIRetry. These cause the retry logic to scale the
// backoff delay from 200ms to around 3.5s.
const (
baseBackoff = float64(200 * time.Millisecond)
backoffMult = 1.618
backoffRetries = 5
)
func backoffAPIRetry(desc, instance string, do func(staleRead time.Time) error) error {
var (
err error
t time.Time
)
for i := 0; i < backoffRetries; i++ {
err = do(t)
gErr, ok := err.(*googleapi.Error)
switch {
case !ok:
// 'ok' will also be false if err is nil.
return err
case gErr.Code == 403 && len(gErr.Errors) > 0 && gErr.Errors[0].Reason == "insufficientPermissions":
// The case where the admin API has not yet been enabled.
return fmt.Errorf("ensure that the Cloud SQL API is enabled for your project (https://console.cloud.google.com/flows/enableapi?apiid=sqladmin). Error during %s %s: %v", desc, instance, err)
case gErr.Code == 404 || gErr.Code == 403:
return fmt.Errorf("ensure that the account has access to %q (and make sure there's no typo in that name). Error during %s %s: %v", instance, desc, instance, err)
case gErr.Code < 500:
// Only Server-level HTTP errors are immediately retryable.
return err
}
// sleep = baseBackoff * backoffMult^(retries + randomFactor)
exp := float64(i+1) + mrand.Float64()
sleep := time.Duration(baseBackoff * math.Pow(backoffMult, exp))
logging.Errorf("Error in %s %s: %v; retrying in %v", desc, instance, err, sleep)
time.Sleep(sleep)
// Create timestamp 30 seconds before now for stale read requests
t = time.Now().UTC().Add(-30 * time.Second)
}
return err
}
func refreshToken(ts oauth2.TokenSource, tok *oauth2.Token) (*oauth2.Token, error) {
expiredToken := &oauth2.Token{
AccessToken: tok.AccessToken,
TokenType: tok.TokenType,
RefreshToken: tok.RefreshToken,
Expiry: time.Time{}.Add(1), // Expired
}
return oauth2.ReuseTokenSource(expiredToken, ts).Token()
}
// Local returns a certificate that may be used to establish a TLS
// connection to the specified instance.
func (s *RemoteCertSource) Local(instance string) (tls.Certificate, error) {
pkix, err := x509.MarshalPKIXPublicKey(s.generateKey().Public())
if err != nil {
return tls.Certificate{}, err
}
p, r, n := util.SplitName(instance)
regionName := fmt.Sprintf("%s~%s", r, n)
pubKey := string(pem.EncodeToMemory(&pem.Block{Bytes: pkix, Type: "RSA PUBLIC KEY"}))
generateEphemeralCertRequest := &sqladmin.GenerateEphemeralCertRequest{
PublicKey: pubKey,
}
var tok *oauth2.Token
// If IAM login is enabled, add the OAuth2 token into the ephemeral
// certificate request.
if s.EnableIAMLogin {
var tokErr error
tok, tokErr = s.TokenSource.Token()
if tokErr != nil {
return tls.Certificate{}, tokErr
}
// Always refresh the token to ensure its expiration is far enough in
// the future.
tok, tokErr = refreshToken(s.TokenSource, tok)
if tokErr != nil {
return tls.Certificate{}, tokErr
}
generateEphemeralCertRequest.AccessToken = tok.AccessToken
}
req := s.serv.Connect.GenerateEphemeralCert(p, regionName, generateEphemeralCertRequest)
var data *sqladmin.GenerateEphemeralCertResponse
err = backoffAPIRetry("generateEphemeral for", instance, func(staleRead time.Time) error {
if !staleRead.IsZero() {
generateEphemeralCertRequest.ReadTime = staleRead.Format(time.RFC3339)
}
data, err = req.Do()
return err
})
if err != nil {
return tls.Certificate{}, err
}
c, err := parseCert(data.EphemeralCert.Cert)
if err != nil {
return tls.Certificate{}, fmt.Errorf("couldn't parse ephemeral certificate for instance %q: %v", instance, err)
}
if s.EnableIAMLogin {
// Adjust the certificate's expiration to be the earlier of tok.Expiry or c.NotAfter
if tok.Expiry.Before(c.NotAfter) {
c.NotAfter = tok.Expiry
}
}
return tls.Certificate{
Certificate: [][]byte{c.Raw},
PrivateKey: s.generateKey(),
Leaf: c,
}, nil
}
func parseCert(pemCert string) (*x509.Certificate, error) {
bl, _ := pem.Decode([]byte(pemCert))
if bl == nil {
return nil, errors.New("invalid PEM: " + pemCert)
}
return x509.ParseCertificate(bl.Bytes)
}
// Return the RSA private key, which is lazily initialized.
func (s *RemoteCertSource) generateKey() *rsa.PrivateKey {
s.keyOnce.Do(func() {
start := time.Now()
pkey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err) // very unexpected.
}
logging.Verbosef("Generated RSA key in %v", time.Since(start))
s.key = pkey
})
return s.key
}
// Find the first matching IP address by user input IP address types
func (s *RemoteCertSource) findIPAddr(data *sqladmin.ConnectSettings, instance string) (ipAddrInUse string, err error) {
for _, eachIPAddrTypeByUser := range s.IPAddrTypes {
for _, eachIPAddrTypeOfInstance := range data.IpAddresses {
if strings.ToUpper(eachIPAddrTypeOfInstance.Type) == strings.ToUpper(eachIPAddrTypeByUser) {
ipAddrInUse = eachIPAddrTypeOfInstance.IpAddress
return ipAddrInUse, nil
}
}
}
ipAddrTypesOfInstance := ""
for _, eachIPAddrTypeOfInstance := range data.IpAddresses {
ipAddrTypesOfInstance += fmt.Sprintf("(TYPE=%v, IP_ADDR=%v)", eachIPAddrTypeOfInstance.Type, eachIPAddrTypeOfInstance.IpAddress)
}
ipAddrTypeOfUser := fmt.Sprintf("%v", s.IPAddrTypes)
return "", fmt.Errorf("User input IP address type %v does not match the instance %v, the instance's IP addresses are %v ", ipAddrTypeOfUser, instance, ipAddrTypesOfInstance)
}
// Remote returns the specified instance's CA certificate, address, and name.
func (s *RemoteCertSource) Remote(instance string) (cert *x509.Certificate, addr, name, version string, err error) {
p, region, n := util.SplitName(instance)
regionName := fmt.Sprintf("%s~%s", region, n)
req := s.serv.Connect.Get(p, regionName)
var data *sqladmin.ConnectSettings
err = backoffAPIRetry("get instance", instance, func(staleRead time.Time) error {
if !staleRead.IsZero() {
req.ReadTime(staleRead.Format(time.RFC3339))
}
data, err = req.Do()
return err
})
if err != nil {
return nil, "", "", "", err
}
// TODO(chowski): remove this when us-central is removed.
if data.Region == "us-central" {
data.Region = "us-central1"
}
if data.Region != region {
if region == "" {
err = fmt.Errorf("instance %v doesn't provide region", instance)
} else {
err = fmt.Errorf(`for connection string "%s": got region %q, want %q`, instance, region, data.Region)
}
if s.checkRegion {
return nil, "", "", "", err
}
logging.Errorf("%v", err)
logging.Errorf("WARNING: specifying the correct region in an instance string will become required in a future version!")
}
if len(data.IpAddresses) == 0 {
return nil, "", "", "", fmt.Errorf("no IP address found for %v", instance)
}
if data.BackendType == "FIRST_GEN" {
logging.Errorf("WARNING: proxy client does not support first generation Cloud SQL instances.")
return nil, "", "", "", fmt.Errorf("%q is a first generation instance", instance)
}
// Find the first matching IP address by user input IP address types
ipAddrInUse := ""
ipAddrInUse, err = s.findIPAddr(data, instance)
if err != nil {
return nil, "", "", "", err
}
c, err := parseCert(data.ServerCaCert.Cert)
return c, ipAddrInUse, p + ":" + n, data.DatabaseVersion, err
}
|