Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mTLS support #974

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ The following arguments are supported:
- `initial_login` - (Optional) Optionally avoid Keycloak login during provider setup, for when Keycloak itself is being provisioned by terraform. Defaults to true, which is the original method.
- `client_timeout` - (Optional) Sets the timeout of the client when addressing Keycloak, in seconds. Defaults to the environment variable `KEYCLOAK_CLIENT_TIMEOUT`, or `5` if the environment variable is not specified.
- `tls_insecure_skip_verify` - (Optional) Allows ignoring insecure certificates when set to `true`. Defaults to `false`. Disabling this security check is dangerous and should only be done in local or test environments.
- `tls_client_certificate` - (Optional) The TLS client certificate in PEM format when the keycloak server is configured with TLS mutual authentication.
- `tls_client_private_key` - (Optional) The TLS client pkcs1 private key in PEM format when the keycloak server is configured with TLS mutual authentication.
- `root_ca_certificate` - (Optional) Allows x509 calls using an unknown CA certificate (for development purposes)
- `base_path` - (Optional) The base path used for accessing the Keycloak REST API. Defaults to the environment variable `KEYCLOAK_BASE_PATH`, or an empty string if the environment variable is not specified. Note that users of the legacy distribution of Keycloak will need to set this attribute to `/auth`.
- `additional_headers` - (Optional) A map of custom HTTP headers to add to each request to the Keycloak API.
18 changes: 13 additions & 5 deletions keycloak/keycloak_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"crypto/x509"
"encoding/json"
"fmt"
"github.com/hashicorp/terraform-plugin-log/tflog"
"io/ioutil"
"net/http"
"net/http/cookiejar"
Expand All @@ -17,6 +16,8 @@ import (
"strings"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"

"github.com/hashicorp/go-version"

"golang.org/x/net/publicsuffix"
Expand Down Expand Up @@ -60,7 +61,7 @@ var redHatSSO7VersionMap = map[int]string{
4: "9.0.17",
}

func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string) (*KeycloakClient, error) {
func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecret, realm, username, password string, initialLogin bool, clientTimeout int, caCert string, tlsInsecureSkipVerify bool, userAgent string, redHatSSO bool, additionalHeaders map[string]string, tlsClientCert string, tlsClientPrivateKey string) (*KeycloakClient, error) {
clientCredentials := &ClientCredentials{
ClientId: clientId,
ClientSecret: clientSecret,
Expand All @@ -79,7 +80,7 @@ func NewKeycloakClient(ctx context.Context, url, basePath, clientId, clientSecre
}
}

httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert)
httpClient, err := newHttpClient(tlsInsecureSkipVerify, clientTimeout, caCert, tlsClientCert, tlsClientPrivateKey)
if err != nil {
return nil, fmt.Errorf("failed to create http client: %v", err)
}
Expand Down Expand Up @@ -170,7 +171,6 @@ func (keycloakClient *KeycloakClient) login(ctx context.Context) error {
serverVersion = strings.ReplaceAll(info.SystemInfo.ServerVersion, ".GA", "")
} else {
regex, err := regexp.Compile(`\.redhat-\w+`)

if err != nil {
fmt.Println("Error compiling regex:", err)
return err
Expand Down Expand Up @@ -499,7 +499,7 @@ func (keycloakClient *KeycloakClient) marshal(body interface{}) ([]byte, error)
return json.Marshal(body)
}

func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string) (*http.Client, error) {
func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string, tlsClientCert string, tlsClientPrivateKey string) (*http.Client, error) {
cookieJar, err := cookiejar.New(&cookiejar.Options{
PublicSuffixList: publicsuffix.List,
})
Expand All @@ -518,6 +518,14 @@ func newHttpClient(tlsInsecureSkipVerify bool, clientTimeout int, caCert string)
transport.TLSClientConfig.RootCAs = caCertPool
}

if tlsClientCert != "" && tlsClientPrivateKey != "" {
clientKeyPairCert, err := tls.X509KeyPair([]byte(tlsClientCert), []byte(tlsClientPrivateKey))
if err != nil {
return nil, err
}
transport.TLSClientConfig.Certificates = []tls.Certificate{clientKeyPairCert}
}

retryClient := retryablehttp.NewClient()
retryClient.RetryMax = 1
retryClient.RetryWaitMin = time.Second * 1
Expand Down
5 changes: 3 additions & 2 deletions keycloak/keycloak_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package keycloak

import (
"context"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"os"
"strconv"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
)

var requiredEnvironmentVariables = []string{
Expand Down Expand Up @@ -50,7 +51,7 @@ func TestAccKeycloakApiClientRefresh(t *testing.T) {

keycloakClient, err := NewKeycloakClient(ctx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), os.Getenv("KEYCLOAK_USER"), os.Getenv("KEYCLOAK_PASSWORD"), true, clientTimeout, "", false, "", false, map[string]string{
"foo": "bar",
})
}, "", "")
if err != nil {
t.Fatalf("%s", err)
}
Expand Down
16 changes: 15 additions & 1 deletion provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,18 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
Description: "Allows ignoring insecure certificates when set to true. Defaults to false. Disabling security check is dangerous and should be avoided.",
Default: false,
},
"tls_client_certificate": {
Optional: true,
Type: schema.TypeString,
Description: "TLS client certificate as PEM string for mutual authentication",
Default: "",
},
"tls_client_private_key": {
Optional: true,
Type: schema.TypeString,
Description: "TLS client private key as PEM string for mutual authentication",
Default: "",
},
"red_hat_sso": {
Optional: true,
Type: schema.TypeBool,
Expand Down Expand Up @@ -210,6 +222,8 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {
initialLogin := data.Get("initial_login").(bool)
clientTimeout := data.Get("client_timeout").(int)
tlsInsecureSkipVerify := data.Get("tls_insecure_skip_verify").(bool)
tlsClientCertificate := data.Get("tls_client_certificate").(string)
tlsClientPrivateKey := data.Get("tls_client_private_key").(string)
rootCaCertificate := data.Get("root_ca_certificate").(string)
redHatSSO := data.Get("red_hat_sso").(bool)
additionalHeaders := make(map[string]string)
Expand All @@ -221,7 +235,7 @@ func KeycloakProvider(client *keycloak.KeycloakClient) *schema.Provider {

userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", provider.TerraformVersion, meta.SDKVersionString())

keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders)
keycloakClient, err := keycloak.NewKeycloakClient(ctx, url, basePath, clientId, clientSecret, realm, username, password, initialLogin, clientTimeout, rootCaCertificate, tlsInsecureSkipVerify, userAgent, redHatSSO, additionalHeaders, tlsClientCertificate, tlsClientPrivateKey)
if err != nil {
diags = append(diags, diag.Diagnostic{
Severity: diag.Error,
Expand Down
23 changes: 13 additions & 10 deletions provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@ package provider
import (
"context"
"fmt"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/meta"
"github.com/mrparkers/terraform-provider-keycloak/keycloak"
"os"
"testing"
)

var testAccProviderFactories map[string]func() (*schema.Provider, error)
var testAccProvider *schema.Provider
var keycloakClient *keycloak.KeycloakClient
var testAccRealm *keycloak.Realm
var testAccRealmTwo *keycloak.Realm
var testAccRealmUserFederation *keycloak.Realm
var testCtx context.Context
var (
testAccProviderFactories map[string]func() (*schema.Provider, error)
testAccProvider *schema.Provider
keycloakClient *keycloak.KeycloakClient
testAccRealm *keycloak.Realm
testAccRealmTwo *keycloak.Realm
testAccRealmUserFederation *keycloak.Realm
testCtx context.Context
)

var requiredEnvironmentVariables = []string{
"KEYCLOAK_CLIENT_ID",
Expand All @@ -31,7 +34,7 @@ func init() {
userAgent := fmt.Sprintf("HashiCorp Terraform/%s (+https://www.terraform.io) Terraform Plugin SDK/%s", schema.Provider{}.TerraformVersion, meta.SDKVersionString())
keycloakClient, _ = keycloak.NewKeycloakClient(testCtx, os.Getenv("KEYCLOAK_URL"), "", os.Getenv("KEYCLOAK_CLIENT_ID"), os.Getenv("KEYCLOAK_CLIENT_SECRET"), os.Getenv("KEYCLOAK_REALM"), "", "", true, 5, "", false, userAgent, false, map[string]string{
"foo": "bar",
})
}, "", "")
testAccProvider = KeycloakProvider(keycloakClient)
testAccProviderFactories = map[string]func() (*schema.Provider, error){
"keycloak": func() (*schema.Provider, error) {
Expand Down
Loading