Browse Source

KeyCloak Progress

* Changes keycloak from ELB Classic to NLB and documents why
* Begins work on keycloak-configuration

Will eventually be tagged v1.25.0
Fred Damstra [afs macbook] 4 years ago
parent
commit
4bab8782f6

+ 132 - 0
base/keycloak-configuration/authentication_flow.tf

@@ -0,0 +1,132 @@
+# Unfortunately, documentation on this is lacking. I started to get close, so i want to leave it, but keycloak configuration is being done by hand.
+#
+# See https://www.keycloak.org/docs/10.0/server_admin/#_x509
+#
+
+#resource "keycloak_authentication_flow" "x509-browser" {
+#  realm_id = keycloak_realm.realm.id
+#  alias    = "X.509 Browser"
+#}
+#
+## Note: the ordering of authentication executions within a flow must be specified using depends_on.
+##
+## Unfortunately, there is very little doc on what 'authenticator's are available. See https://github.com/mrparkers/terraform-provider-keycloak/issues/411
+## But there are some examples in https://github.com/mrparkers/terraform-provider-keycloak/blob/master/example/main.tf
+#resource "keycloak_authentication_execution" "execution_1" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+#  authenticator     = "auth-cookie"
+#  requirement       = "ALTERNATIVE"
+#}
+#
+#resource "keycloak_authentication_execution" "execution_2" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+#  authenticator     = "auth-spnego" # "kerberos"
+#  requirement       = "DISABLED"
+#
+#  depends_on = [
+#    keycloak_authentication_execution.execution_1
+#  ]
+#}
+#
+#resource "keycloak_authentication_execution" "execution_3" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+#  authenticator     = "identity-provider-redirector"
+#  requirement       = "ALTERNATIVE"
+#
+#  depends_on = [
+#    keycloak_authentication_execution.execution_2
+#  ]
+#}
+#
+#resource "keycloak_authentication_execution" "execution_3" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+#  authenticator     = "identity-provider-redirector"
+#  requirement       = "ALTERNATIVE"
+#
+#  depends_on = [
+#    keycloak_authentication_execution.execution_2
+#  ]
+#}
+#
+#resource "keycloak_authentication_subflow" "subflow_3" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+#  alias             = "browser-copy-flow-forms"
+#  requirement       = "ALTERNATIVE"
+#  depends_on        = [
+#    keycloak_authentication_execution.execution_3
+#  ]
+#}
+#
+#resource "keycloak_authentication_execution" "execution_4" {
+#  realm_id          = keycloak_realm.realm.id
+#  parent_flow_alias = keycloak_authentication_subflow.subflow_3.alias
+#
+#  authenticator     = "auth-username-password-form"
+#  requirement       = "REQUIRED"
+#  depends_on        = [
+#    keycloak_authentication_subflow.subflow_3
+#  ]
+#}
+#
+## No OTPs for us?
+##resource "keycloak_authentication_execution" "execution_6" {
+##  realm_id          = keycloak_realm.realm.id
+##  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+##  authenticator     = "auth-otp-form"
+##  requirement       = "REQUIRED"
+##  depends_on        = [
+##    keycloak_authentication_execution.execution_3
+##  ]
+##}
+#
+##resource "keycloak_authentication_execution_config" "config" {
+##  realm_id          = keycloak_realm.realm.id
+##  parent_flow_alias = keycloak_authentication_flow.x509-browser.alias
+##  alias        = "idp-XXX-config"
+##  config       = {
+##    defaultProvider = "idp-XXX"
+##  }
+##  depends_on        = [
+##    keycloak_authentication_execution.execution_3
+##  ]
+##}
+##
+
+
+
+#TODO:
+#resource "keycloak_openid_client" "test_client" {
+#  client_id   = "test-openid-client"
+#  name        = "test-openid-client"
+#  realm_id    = keycloak_realm.test.id
+#  description = "a test openid client"
+#
+#  standard_flow_enabled    = true
+#  service_accounts_enabled = true
+#
+#  access_type = "CONFIDENTIAL"
+#
+#  valid_redirect_uris = [
+#    "http://localhost:5555/callback",
+#  ]
+#
+#  client_secret = "secret"
+#
+#  pkce_code_challenge_method = "plain"
+#
+#  login_theme = "keycloak"
+#}
+
+
+#resource "keycloak_required_action" "custom-terms-and-conditions" {
+#  realm_id       = keycloak_realm.realm.realm
+#  alias          = "terms_and_conditions"
+#  default_action = true
+#  enabled        = true
+#  name           = "Custom Terms and Conditions"
+#}

+ 3 - 0
base/keycloak-configuration/outputs.tf

@@ -0,0 +1,3 @@
+#output fredwashere {
+#  value = "Fred was here"
+#}

+ 91 - 0
base/keycloak-configuration/realm.tf

@@ -0,0 +1,91 @@
+resource "keycloak_realm" "realm" {
+  # Docs: https://registry.terraform.io/providers/mrparkers/keycloak/latest/docs/resources/realm
+  realm             = "XDR"
+  enabled           = true
+  display_name      = "AFS eXtended Detection and Response"
+  display_name_html = "<b>AFS XDR</b>"
+
+  user_managed_access = false
+
+  #login_theme = "base"
+  # account_theme = ""
+  # admin_theme = ""
+  # email_theme = ""
+
+  registration_allowed = false
+  edit_username_allowed = true
+  reset_password_allowed = false
+  remember_me = false
+  verify_email = true
+  login_with_email_allowed = true
+  duplicate_emails_allowed = false
+  ssl_required = "all"
+
+  # default_signature_algorithm = ""?
+  # revoke_refresh_token = ""
+  # refresh_token_max_reuse = ""
+
+  # TODO: Wes, Brad, Asha or somebody better should review these:
+  sso_session_idle_timeout = "1h" # (Optional) The amount of time a session can be idle before it expires.
+  sso_session_max_lifespan = "8h" # (Optional) The maximum amount of time before a session expires regardless of activity.
+  # offline_session_idle_timeout = "" # (Optional) The amount of time an offline session can be idle before it expires.
+  # offline_session_max_lifespan = "" # (Optional) The maximum amount of time before an offline session expires regardless of activity.
+  # offline_session_max_lifespan_enabled = "" # (Optional) Enable offline_session_max_lifespan.
+  #access_token_lifespan = "1h" # (Optional) The amount of time an access token can be used before it expires.
+  # access_token_lifespan_for_implicit_flow = "" # (Optional) The amount of time an access token issued with the OpenID Connect Implicit Flow can be used before it expires.
+  # access_code_lifespan = "" # (Optional) The maximum amount of time a client has to finish the authorization code flow.
+  # access_code_lifespan_login = "" # (Optional) The maximum amount of time a user is permitted to stay on the login page before the authentication process must be restarted.
+  # access_code_lifespan_user_action = "" # (Optional) The maximum amount of time a user has to complete login related actions, such as updating a password.
+  # action_token_generated_by_user_lifespan = "" # (Optional) The maximum time a user has to use a user-generated permit before it expires.
+  # action_token_generated_by_admin_lifespan = "" # (Optional) The maximum time a user has to use an admin-generated permit before it expires.
+
+  password_policy = "upperCase(1) and length(12) and forceExpiredPasswordChange(90) and notUsername"
+
+  smtp_server {
+    host = "mailrelay.${ var.dns_info["private"]["zone"] }"
+    from = "keycloak@${ var.dns_info["public"]["zone"] }"
+    from_display_name = "AFS XDR KeyCloak"
+    reply_to = "xdr.eng@accenturefederal.com"
+    reply_to_display_name = "XDR Engineering"
+  }
+
+  #attributes      = {
+  #  mycustomAttribute = "myCustomValue"
+  #}
+
+  internationalization {
+    supported_locales = [
+      "en",
+      "de",
+      "es"
+    ]
+    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"
+    }
+    brute_force_detection {
+      permanent_lockout  = false# (Optional) When true, this will lock the user permanently when the user exceeds the maximum login failures.
+      max_login_failures  = 3 # (Optional) How many failures before wait is triggered.
+      wait_increment_seconds  = 60 # (Optional) This represents the amount of time a user should be locked out when the login failure threshold has been met.
+      quick_login_check_milli_seconds  = 1000 # (Optional) Configures the amount of time, in milliseconds, for consecutive failures to lock a user out.
+      minimum_quick_login_wait_seconds  = 60 # (Optional) How long to wait after a quick login failure.
+      max_failure_wait_seconds  = 900 # (Optional) Max. time a user will be locked out.
+      failure_reset_time_seconds  = 43200 # (Optional) When will failure count be reset?
+    }
+  }
+
+  #web_authn_policy {
+  #  relying_party_entity_name = "Example"
+  #  relying_party_id          = "keycloak.example.com"
+  #  signature_algorithms      = ["ES256", "RS256"]
+  #}
+}

+ 24 - 0
base/keycloak-configuration/vars.tf

@@ -0,0 +1,24 @@
+variable "tags" {
+  description = "Tags to add to the resource (in addition to global standard tags)"
+  type        = map
+  default     = { }
+}
+
+variable "trusted_ips" { type = list(string) }
+variable "xdr_interconnect" { type = list(string) }
+variable "nga_pop" { type = list(string) }
+variable "afs_azure_pop" { type = list(string) }
+variable "afs_pop" { type = list(string) }
+variable "proxy" { type = string }
+variable "salt_master" { type = string }
+
+variable "cidr_map" { type = map }
+variable "dns_info" { type = map }
+variable "standard_tags" { type = map }
+variable "environment" { type = string }
+variable "aws_region" { type = string }
+variable "aws_partition" { type = string }
+variable "aws_partition_alias" { type = string }
+variable "aws_account_id" { type = string }
+variable "common_services_account" { type = string }
+variable "instance_termination_protection" { type = bool }

+ 0 - 0
base/keycloak/elbclassic.tf → base/keycloak/elbclassic.tf.skipped


+ 75 - 0
base/keycloak/nlb.tf

@@ -0,0 +1,75 @@
+# KeyCloak Needs an NLB:
+#   * ALB/ELB can't terminate SSL, because Keycloak needs the certificate
+#   * Because they don't terminate SSL, they can't provide X-forwarded-for, and keycloak needs the source IP
+#   * Therefore, we use an NLB and preserve the source IP.
+module "public_dns_record" {
+  source = "../../submodules/dns/public_ALIAS_record"
+
+  name = "keycloak.${var.dns_info["public"]["zone"]}"
+  target_dns_name = aws_lb.external.dns_name
+  target_zone_id  = aws_lb.external.zone_id
+  dns_info = var.dns_info
+
+  providers = {
+    aws.mdr-common-services-commercial = aws.mdr-common-services-commercial
+  }
+}
+
+resource "aws_lb" "external" {
+  name = "keycloak-external-nlb"
+  load_balancer_type = "network"
+  internal = false
+  subnets = var.public_subnets
+
+  access_logs {
+    bucket  = "xdr-elb-${ var.environment }"
+    enabled = true
+  }
+
+  enable_cross_zone_load_balancing = true
+  idle_timeout                = 300
+
+  tags = merge(var.standard_tags, var.tags)
+}
+
+resource "aws_lb_listener" "nlb_443" {
+  load_balancer_arn = aws_lb.external.arn
+  port              = "443"
+  protocol          = "TCP"
+
+  default_action {
+    type             = "forward"
+    target_group_arn = aws_lb_target_group.external.arn
+  }
+}
+
+resource "aws_lb_target_group" "external" {
+  name     = "keycloak-external-nlb"
+  port     = 8443
+  protocol = "TCP"
+  vpc_id   = var.vpc_id
+  target_type = "instance"
+
+  health_check {
+    enabled = true
+    #healthy_threshold   = 3
+    #unhealthy_threshold = 2
+    timeout = 10
+    interval = 10
+    #matcher = "200,302"
+    path = "/"
+    protocol = "HTTPS"
+  }
+
+  stickiness {
+    enabled = true
+    type = "source_ip" # only option for NLBs
+  }
+}
+
+# Create a new load balancer attachment
+resource "aws_lb_target_group_attachment" "external_attachment" {
+  count = var.keycloak_instance_count
+  target_group_arn = aws_lb_target_group.external.arn
+  target_id = aws_instance.instance[count.index].id
+}

+ 3 - 3
base/keycloak/outputs.tf

@@ -15,6 +15,6 @@ output db_endpoint {
 #  value = aws_eip.instance.public_ip
 #}
 #
-#output instance_private_ip {
-#  value = aws_instance.instance.private_ip
-#}
+output instance_private_ip {
+  value = aws_instance.instance[*].private_ip
+}

+ 0 - 0
base/keycloak/security-groups-elb.tf → base/keycloak/security-groups-elb.tf.skipped


+ 0 - 0
base/keycloak/rds-security-groups.tf → base/keycloak/security-groups-rds.tf


+ 23 - 16
base/keycloak/security-groups.tf

@@ -10,7 +10,6 @@ data "aws_security_group" "aws_endpoints" {
   vpc_id = var.vpc_id
 }
 
-# For now, opening everything:
 #   ajp port: 8009
 #   http: 8080
 #   https: 8443
@@ -18,8 +17,6 @@ data "aws_security_group" "aws_endpoints" {
 #   mgmt-https: 9993
 #   txn-recovery-environment: 4712
 #   txn-status-manager: 4713
-#
-#   Also opening 80 and 443 for certbot
 
 resource "aws_security_group" "instance" {
   name = "Keycloak"
@@ -89,15 +86,15 @@ resource "aws_security_group_rule" "instance-alt-http-in-from-access" {
   security_group_id = aws_security_group.instance.id
 }
 
-resource "aws_security_group_rule" "instance-alt-http-in-from-elb" {
-  description = "Alt HTTP from ELB"
-  type = "ingress"
-  from_port = "8080"
-  to_port = "8080"
-  protocol = "tcp"
-  security_group_id = aws_security_group.instance.id
-  source_security_group_id = aws_security_group.elb_external.id
-}
+#resource "aws_security_group_rule" "instance-alt-http-in-from-elb" {
+#  description = "Alt HTTP from ELB"
+#  type = "ingress"
+#  from_port = "8080"
+#  to_port = "8080"
+#  protocol = "tcp"
+#  security_group_id = aws_security_group.instance.id
+#  source_security_group_id = aws_security_group.elb_external.id
+#}
 
 resource "aws_security_group_rule" "instance-alt-https-in-from-access" {
   description = "Alt HTTPS from Access"
@@ -109,14 +106,24 @@ resource "aws_security_group_rule" "instance-alt-https-in-from-access" {
   security_group_id = aws_security_group.instance.id
 }
 
-resource "aws_security_group_rule" "instance-alt-https-in-from-elb" {
-  description = "Alt HTTPS from ELB"
+resource "aws_security_group_rule" "instance-alt-https-in-from-nlb" {
+  description = "Alt HTTPS from Internet"
   type = "ingress"
   from_port = "8443"
   to_port = "8443"
   protocol = "tcp"
+  cidr_blocks = [ "0.0.0.0/0" ]
+  security_group_id = aws_security_group.instance.id
+}
+
+resource "aws_security_group_rule" "instance-mgmt-in-from-access" {
+  description = "Management HTTPS from Access"
+  type = "ingress"
+  from_port = "9990"
+  to_port = "9990"
+  protocol = "tcp"
+  cidr_blocks = var.cidr_map["vpc-access"]
   security_group_id = aws_security_group.instance.id
-  source_security_group_id = aws_security_group.elb_external.id
 }
 
 resource "aws_security_group_rule" "instance-db-outbound" {
@@ -126,7 +133,7 @@ resource "aws_security_group_rule" "instance-db-outbound" {
   to_port = "5432"
   protocol = "tcp"
   security_group_id = aws_security_group.instance.id
-  source_security_group_id = data.aws_security_group.aws_endpoints.id
+  source_security_group_id = aws_security_group.keycloak_rds_sg.id
 }