Skip to content

Commit

Permalink
feat: adds browser headers security defenses for keycloak_realm (#130)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomrutsaert authored and mrparkers committed Jul 8, 2019
1 parent 9209197 commit fec23b4
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 1 deletion.
14 changes: 13 additions & 1 deletion example/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ resource "keycloak_realm" "test" {
}
}

account_theme = "base"
account_theme = "base"

access_code_lifespan = "30m"

Expand All @@ -38,6 +38,18 @@ resource "keycloak_realm" "test" {
]
default_locale = "en"
}

security_defenses {
headers {
x_frame_options = "DENY"
content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
content_security_policy_report_only = ""
x_content_type_options = "nosniff"
x_robots_tag = "none"
x_xss_protection = "1; mode=block"
strict_transport_security = "max-age=31536000; includeSubDomains"
}
}
}

resource "keycloak_required_action" "custom-terms-and-conditions" {
Expand Down
13 changes: 13 additions & 0 deletions keycloak/realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ type Realm struct {
InternationalizationEnabled bool `json:"internationalizationEnabled"`
SupportLocales []string `json:"supportedLocales"`
DefaultLocale string `json:"defaultLocale"`

//extra attributes of a realm, contains security defenses browser headers and brute force detection parameters(those still nee to be added)
Attributes Attributes `json:"attributes,omitempty"`
}

type Attributes struct {
BrowserHeaderContentSecurityPolicy string `json:"_browser_header.contentSecurityPolicy,omitempty"`
BrowserHeaderContentSecurityPolicyReportOnly string `json:"_browser_header.contentSecurityPolicyReportOnly,omitempty"`
BrowserHeaderStrictTransportSecurity string `json:"_browser_header.strictTransportSecurity,omitempty"`
BrowserHeaderXContentTypeOptions string `json:"_browser_header.xContentTypeOptions,omitempty"`
BrowserHeaderXFrameOptions string `json:"_browser_header.xFrameOptions,omitempty"`
BrowserHeaderXRobotsTag string `json:"_browser_header.xRobotsTag,omitempty"`
BrowserHeaderXXSSProtection string `json:"_browser_header.xXSSProtection,omitempty"`
}

type SmtpServer struct {
Expand Down
113 changes: 113 additions & 0 deletions provider/resource_keycloak_realm.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,61 @@ func resourceKeycloakRealm() *schema.Resource {
},
},
},

//Security Defenses
"security_defenses": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"headers": {
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"x_frame_options": {
Type: schema.TypeString,
Optional: true,
Default: "SAMEORIGIN",
},
"content_security_policy": {
Type: schema.TypeString,
Optional: true,
Default: "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
},
"content_security_policy_report_only": {
Type: schema.TypeString,
Optional: true,
Default: "",
},
"x_content_type_options": {
Type: schema.TypeString,
Optional: true,
Default: "nosniff",
},
"x_robots_tag": {
Type: schema.TypeString,
Optional: true,
Default: "none",
},
"x_xss_protection": {
Type: schema.TypeString,
Optional: true,
Default: "1; mode=block",
},
"strict_transport_security": {
Type: schema.TypeString,
Optional: true,
Default: "max-age=31536000; includeSubDomains",
},
},
},
},
},
},
},
},
}
}
Expand Down Expand Up @@ -456,9 +511,44 @@ func getRealmFromData(data *schema.ResourceData) (*keycloak.Realm, error) {
realm.ActionTokenGeneratedByAdminLifespan = actionTokenGeneratedByAdminLifespanDurationString
}

//security defenses
if v, ok := data.GetOk("security_defenses"); ok {
securityDefensesSettings := v.([]interface{})[0].(map[string]interface{})

headersConfig := securityDefensesSettings["headers"].([]interface{})
if len(headersConfig) == 1 {
headerSettings := headersConfig[0].(map[string]interface{})

realm.Attributes = keycloak.Attributes{
BrowserHeaderContentSecurityPolicy: headerSettings["content_security_policy"].(string),
BrowserHeaderContentSecurityPolicyReportOnly: headerSettings["content_security_policy_report_only"].(string),
BrowserHeaderStrictTransportSecurity: headerSettings["strict_transport_security"].(string),
BrowserHeaderXContentTypeOptions: headerSettings["x_content_type_options"].(string),
BrowserHeaderXFrameOptions: headerSettings["x_frame_options"].(string),
BrowserHeaderXRobotsTag: headerSettings["x_robots_tag"].(string),
BrowserHeaderXXSSProtection: headerSettings["x_xss_protection"].(string),
}
} else {
setDefaultSecuritySettings(realm)
}
} else {
setDefaultSecuritySettings(realm)
}
return realm, nil
}

func setDefaultSecuritySettings(realm *keycloak.Realm) {
realm.Attributes = keycloak.Attributes{
BrowserHeaderContentSecurityPolicy: "frame-src 'self'; frame-ancestors 'self'; object-src 'none';",
BrowserHeaderContentSecurityPolicyReportOnly: "",
BrowserHeaderStrictTransportSecurity: "max-age=31536000; includeSubDomains",
BrowserHeaderXContentTypeOptions: "nosniff",
BrowserHeaderXFrameOptions: "SAMEORIGIN",
BrowserHeaderXRobotsTag: "none",
BrowserHeaderXXSSProtection: "1; mode=block",
}
}

func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
data.SetId(realm.Realm)

Expand Down Expand Up @@ -534,6 +624,29 @@ func setRealmData(data *schema.ResourceData, realm *keycloak.Realm) {
} else {
data.Set("internationalization", nil)
}

if _, ok := data.GetOk("security_defenses"); ok {

if (keycloak.Attributes{}) == realm.Attributes {
data.Set("security_defenses", nil)
} else {
securityDefensesSettings := make(map[string]interface{})

headersSettings := make(map[string]interface{})

headersSettings["content_security_policy"] = realm.Attributes.BrowserHeaderContentSecurityPolicy
headersSettings["content_security_policy_report_only"] = realm.Attributes.BrowserHeaderContentSecurityPolicyReportOnly
headersSettings["strict_transport_security"] = realm.Attributes.BrowserHeaderStrictTransportSecurity
headersSettings["x_content_type_options"] = realm.Attributes.BrowserHeaderXContentTypeOptions
headersSettings["x_frame_options"] = realm.Attributes.BrowserHeaderXFrameOptions
headersSettings["x_robots_tag"] = realm.Attributes.BrowserHeaderXRobotsTag
headersSettings["x_xss_protection"] = realm.Attributes.BrowserHeaderXXSSProtection

securityDefensesSettings["headers"] = []interface{}{headersSettings}

data.Set("security_defenses", []interface{}{securityDefensesSettings})
}
}
}

func resourceKeycloakRealmCreate(data *schema.ResourceData, meta interface{}) error {
Expand Down
65 changes: 65 additions & 0 deletions provider/resource_keycloak_realm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,35 @@ func TestAccKeycloakRealm_computedTokenSettings(t *testing.T) {
})
}

func TestAccKeycloakRealm_securityDefenses(t *testing.T) {
realmName := "terraform-" + acctest.RandString(10)
realmDisplayName := "terraform-" + acctest.RandString(10)

resource.Test(t, resource.TestCase{
Providers: testAccProviders,
PreCheck: func() { testAccPreCheck(t) },
CheckDestroy: testAccCheckKeycloakRealmDestroy(),
Steps: []resource.TestStep{
{
Config: testKeycloakRealm_basic(realmName, realmDisplayName),
Check: testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
},
{
Config: testKeycloakRealm_securityDefenses(realmName, realmDisplayName, "SAMEORIGIN"),
Check: testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
},
{
Config: testKeycloakRealm_securityDefenses(realmName, realmDisplayName, "DENY"),
Check: testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "DENY"),
},
{
Config: testKeycloakRealm_basic(realmName, realmDisplayName),
Check: testAccCheckKeycloakRealmSecurityDefenses("keycloak_realm.realm", "SAMEORIGIN"),
},
},
})
}

func testKeycloakRealmLoginInfo(resourceName string, realm *keycloak.Realm) resource.TestCheckFunc {
return func(s *terraform.State) error {
realmFromState, err := getRealmFromState(s, resourceName)
Expand Down Expand Up @@ -607,6 +636,21 @@ func getRealmFromState(s *terraform.State, resourceName string) (*keycloak.Realm
return realm, nil
}

func testAccCheckKeycloakRealmSecurityDefenses(resourceName, xFrameOptions string) resource.TestCheckFunc {
return func(s *terraform.State) error {
realm, err := getRealmFromState(s, resourceName)
if err != nil {
return err
}

if realm.Attributes.BrowserHeaderXFrameOptions != xFrameOptions {
return fmt.Errorf("expected realm %s to have attribute _browser_header.xFrameOptions set to %s, but was %s", realm.Realm, xFrameOptions, realm.Attributes.BrowserHeaderXFrameOptions)
}

return nil
}
}

func testKeycloakRealm_basic(realm, realmDisplayName string) string {
return fmt.Sprintf(`
resource "keycloak_realm" "realm" {
Expand Down Expand Up @@ -839,3 +883,24 @@ resource "keycloak_realm" "realm" {
}
`, realm, realm, ssoSessionIdleTimeout, ssoSessionMaxLifespan, offlineSessionIdleTimeout, offlineSessionMaxLifespan, accessTokenLifespan, accessTokenLifespanForImplicitFlow, accessCodeLifespan, accessCodeLifespanLogin, accessCodeLifespanUserAction, actionTokenGeneratedByUserLifespan, actionTokenGeneratedByAdminLifespan)
}

func testKeycloakRealm_securityDefenses(realm, realmDisplayName, xFrameOptions string) string {
return fmt.Sprintf(`
resource "keycloak_realm" "realm" {
realm = "%s"
enabled = true
display_name = "%s"
security_defenses {
headers {
x_frame_options = "%s"
content_security_policy = "frame-src 'self'; frame-ancestors 'self'; object-src 'none';"
content_security_policy_report_only = ""
x_content_type_options = "nosniff"
x_robots_tag = "none"
x_xss_protection = "1; mode=block"
strict_transport_security = "max-age=31536000; includeSubDomains"
}
}
}
`, realm, realmDisplayName, xFrameOptions)
}

0 comments on commit fec23b4

Please sign in to comment.