Skip to content

Commit 04da235

Browse files
Ivan De Marinobflad
andauthored
Support for Proxy on the Provider level (#179)
* Validating tls_certificate.url argument using validate.IsURLWithHTTPS * Add `goproxy` dependency for testing * Add proxy configuration to the provider * Add support for 2 kinds of URL Scheme for `tls_certificate` data source `https://` (that follows optional http proxy configuration) and `tls://` that opens a secure socket connection directly to the destination * Regenerate documentation * util_test.go -> local_server_test.go * Indicate that `from_env` will be set by default to `true` from the next major release * Follow up issue: #183 * For now, we use `http.Client` only when a proxy configuration is present * Follow up issue to clean this up: #183 * `tls_certificate` certificate fetching via HTTP: attempt to use `HEAD` and, if that fails, `GET` * Add test to configure proxy from environment * Test to confirm behaviour when a configured proxy can't actually be reached * Typo fixes in `internal/provider/data_source_certificate.go` Co-authored-by: Brian Flad <bflad417@gmail.com>
1 parent 45b31f5 commit 04da235

13 files changed

Lines changed: 881 additions & 83 deletions

docs/data-sources/certificate.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@ page_title: "tls_certificate Data Source - terraform-provider-tls"
33
subcategory: ""
44
description: |-
55
Get information about the TLS certificates securing a host.
6-
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects an HTTPS website.
6+
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects a URL.
77
---
88

99
# tls_certificate (Data Source)
1010

1111
Get information about the TLS certificates securing a host.
1212

13-
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects an HTTPS website.
13+
Use this data source to get information, such as SHA1 fingerprint or serial number, about the TLS certificates that protects a URL.
1414

1515
## Example Usage
1616

docs/index.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,57 @@ resource "aws_iam_server_certificate" "example" {
7070
}
7171
```
7272

73+
### Configuring Proxy
74+
75+
```terraform
76+
# This example fetches the TLS certificate chain
77+
# from `example.com` using an HTTP Proxy.
78+
79+
provider "tls" {
80+
proxy {
81+
url = "https://corporate.proxy.service"
82+
}
83+
}
84+
85+
data "tls_certificate" "test" {
86+
url = "https://example.com"
87+
}
88+
```
89+
90+
```terraform
91+
# This example fetches the TLS certificate chain
92+
# from `example.com` using an HTTP Proxy.
93+
# The Proxy is discovered via environment variables:
94+
# see https://pkg.go.dev/net/http#ProxyFromEnvironment for details.
95+
96+
provider "tls" {
97+
proxy {
98+
from_env = true
99+
}
100+
}
101+
102+
data "tls_certificate" "test" {
103+
url = "https://example.com"
104+
}
105+
```
106+
107+
<!-- schema generated by tfplugindocs -->
108+
## Schema
109+
110+
### Optional
111+
112+
- `proxy` (Block List, Max: 1) Proxy used by resources and data sources that connect to external endpoints. (see [below for nested schema](#nestedblock--proxy))
113+
114+
<a id="nestedblock--proxy"></a>
115+
### Nested Schema for `proxy`
116+
117+
Optional:
118+
119+
- `from_env` (Boolean) When `true` the provider will discover the proxy configuration from environment variables. This is based upon [`http.ProxyFromEnvironment`](https://pkg.go.dev/net/http#ProxyFromEnvironment) and it supports the same environment variables (default: `false`). **NOTE**: the default value for this argument will be change to `true` in the next major release.
120+
- `password` (String, Sensitive) Password used for Basic authentication against the Proxy.
121+
- `url` (String) URL used to connect to the Proxy. Accepted schemes are: `http`, `https`, `socks5`.
122+
- `username` (String) Username (or Token) used for Basic authentication against the Proxy.
123+
73124
## Limitations
74125

75126
### `ECDSA` with `P224` elliptic curve
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# This example fetches the TLS certificate chain
2+
# from `example.com` using an HTTP Proxy.
3+
4+
provider "tls" {
5+
proxy {
6+
url = "https://corporate.proxy.service"
7+
}
8+
}
9+
10+
data "tls_certificate" "test" {
11+
url = "https://example.com"
12+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# This example fetches the TLS certificate chain
2+
# from `example.com` using an HTTP Proxy.
3+
# The Proxy is discovered via environment variables:
4+
# see https://pkg.go.dev/net/http#ProxyFromEnvironment for details.
5+
6+
provider "tls" {
7+
proxy {
8+
from_env = true
9+
}
10+
}
11+
12+
data "tls_certificate" "test" {
13+
url = "https://example.com"
14+
}

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ module github.com/terraform-providers/terraform-provider-tls
33
go 1.17
44

55
require (
6+
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e
7+
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2
68
github.com/hashicorp/terraform-plugin-docs v0.7.0
79
github.com/hashicorp/terraform-plugin-sdk/v2 v2.13.0
810
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
4747
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4848
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
4949
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
50+
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e h1:99KFda6F/mw8xSfceY2JEVCrYWX7l+Ms6BcO5wEct+Q=
51+
github.com/elazarl/goproxy v0.0.0-20220328115640-894aeddb713e/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
52+
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM=
53+
github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8=
5054
github.com/emirpasic/gods v1.12.0 h1:QAUIPSaCu4G+POclxeqb3F+WPpdKqFGlw36+yOzGlrg=
5155
github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o=
5256
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
@@ -215,6 +219,7 @@ github.com/posener/complete v1.1.1 h1:ccV59UEOTzVDnDUEFdT95ZzHVZ+5+158q8+SJb2QV5
215219
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
216220
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
217221
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
222+
github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc=
218223
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
219224
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
220225
github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4=

internal/provider/data_source_certificate.go

Lines changed: 89 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package provider
22

33
import (
44
"crypto/tls"
5+
"crypto/x509"
56
"fmt"
7+
"net/http"
68
"net/url"
9+
"strings"
710
"time"
811

912
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
13+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
1014
)
1115

1216
func dataSourceCertificate() *schema.Resource {
@@ -15,13 +19,17 @@ func dataSourceCertificate() *schema.Resource {
1519

1620
Description: "Get information about the TLS certificates securing a host.\n\n" +
1721
"Use this data source to get information, such as SHA1 fingerprint or serial number, " +
18-
"about the TLS certificates that protects an HTTPS website.",
22+
"about the TLS certificates that protects a URL.",
1923

2024
Schema: map[string]*schema.Schema{
2125
"url": {
22-
Type: schema.TypeString,
23-
Required: true,
24-
Description: "The URL of the website to get the certificates from.",
26+
Type: schema.TypeString,
27+
Required: true,
28+
Description: "URL of the endpoint to get the certificates from. " +
29+
fmt.Sprintf("Accepted schemes are: `%s`. ", strings.Join(SupportedURLSchemesStr(), "`, `")) +
30+
"For scheme `https://` it will use the HTTP protocol and apply the `proxy` configuration " +
31+
"of the provider, if set. For scheme `tls://` it will instead use a secure TCP socket.",
32+
ValidateDiagFunc: validation.ToDiagFunc(validation.IsURLWithScheme(SupportedURLSchemesStr())),
2533
},
2634
"verify_chain": {
2735
Type: schema.TypeBool,
@@ -104,32 +112,51 @@ func dataSourceCertificate() *schema.Resource {
104112
}
105113
}
106114

107-
func dataSourceCertificateRead(d *schema.ResourceData, _ interface{}) error {
108-
u, err := url.Parse(d.Get("url").(string))
115+
func dataSourceCertificateRead(d *schema.ResourceData, m interface{}) error {
116+
config := m.(*providerConfig)
117+
118+
targetURL, err := url.Parse(d.Get("url").(string))
109119
if err != nil {
110120
return err
111121
}
112-
if u.Scheme != "https" {
113-
return fmt.Errorf("invalid scheme")
114-
}
115-
if u.Port() == "" {
116-
u.Host += ":443"
117-
}
118122

119-
verifyChain := d.Get("verify_chain").(bool)
123+
// Determine if we should verify the chain of certificates, or skip said verification
124+
shouldVerifyChain := d.Get("verify_chain").(bool)
120125

121-
conn, err := tls.Dial("tcp", u.Host, &tls.Config{InsecureSkipVerify: !verifyChain})
126+
// Ensure a port is set on the URL, or return an error
127+
var peerCerts []*x509.Certificate
128+
switch targetURL.Scheme {
129+
case HTTPSScheme.String():
130+
if targetURL.Port() == "" {
131+
targetURL.Host += ":443"
132+
}
133+
134+
// TODO remove this branch and default to use `fetchPeerCertificatesViaHTTPS`
135+
// as part of https://github.com/hashicorp/terraform-provider-tls/issues/183
136+
if config.isProxyConfigured() {
137+
peerCerts, err = fetchPeerCertificatesViaHTTPS(targetURL, shouldVerifyChain, config)
138+
} else {
139+
peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain)
140+
}
141+
case TLSScheme.String():
142+
if targetURL.Port() == "" {
143+
return fmt.Errorf("port missing from URL: %s", targetURL.String())
144+
}
145+
146+
peerCerts, err = fetchPeerCertificatesViaTLS(targetURL, shouldVerifyChain)
147+
default:
148+
// NOTE: This should never happen, given we validate this at the schema level
149+
return fmt.Errorf("unsupported scheme: %s", targetURL.Scheme)
150+
}
122151
if err != nil {
123152
return err
124153
}
125-
defer conn.Close()
126-
state := conn.ConnectionState()
127154

128-
var certs []interface{}
129-
for i := len(state.PeerCertificates) - 1; i >= 0; i-- {
130-
certs = append(certs, certificateToMap(state.PeerCertificates[i]))
155+
// Convert peer certificates to a simple map
156+
certs := make([]interface{}, len(peerCerts))
157+
for i, peerCert := range peerCerts {
158+
certs[len(peerCerts)-i-1] = certificateToMap(peerCert)
131159
}
132-
133160
err = d.Set("certificates", certs)
134161
if err != nil {
135162
return err
@@ -139,3 +166,45 @@ func dataSourceCertificateRead(d *schema.ResourceData, _ interface{}) error {
139166

140167
return nil
141168
}
169+
170+
func fetchPeerCertificatesViaTLS(targetURL *url.URL, shouldVerifyChain bool) ([]*x509.Certificate, error) {
171+
conn, err := tls.Dial("tcp", targetURL.Host, &tls.Config{
172+
InsecureSkipVerify: !shouldVerifyChain,
173+
})
174+
if err != nil {
175+
return nil, fmt.Errorf("unable to execute TLS connection towards %s: %w", targetURL.Host, err)
176+
}
177+
defer conn.Close()
178+
179+
return conn.ConnectionState().PeerCertificates, nil
180+
}
181+
182+
func fetchPeerCertificatesViaHTTPS(targetURL *url.URL, shouldVerifyChain bool, config *providerConfig) ([]*x509.Certificate, error) {
183+
client := &http.Client{
184+
Transport: &http.Transport{
185+
TLSClientConfig: &tls.Config{
186+
InsecureSkipVerify: !shouldVerifyChain,
187+
},
188+
Proxy: config.proxyForRequestFunc(),
189+
},
190+
}
191+
192+
// First attempting an HTTP HEAD: if it fails, ignore errors and move on
193+
resp, err := client.Head(targetURL.String())
194+
if err == nil && resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
195+
defer resp.Body.Close()
196+
return resp.TLS.PeerCertificates, nil
197+
}
198+
199+
// Then attempting HTTP GET: if this fails we will than report the error
200+
resp, err = client.Get(targetURL.String())
201+
if err != nil {
202+
return nil, fmt.Errorf("failed to fetch certificates from URL '%s': %w", targetURL.Scheme, err)
203+
}
204+
defer resp.Body.Close()
205+
if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 {
206+
return resp.TLS.PeerCertificates, nil
207+
}
208+
209+
return nil, fmt.Errorf("got back response (status: %s) with no certificates from URL '%s': %w", resp.Status, targetURL.Scheme, err)
210+
}

0 commit comments

Comments
 (0)