Deploy keycloak server on AWS using Terraform

Deploy keycloak server on AWS using Terraform

·

6 min read

Hello IT guys, i m pleased to start my first blog. All yours feedback are very welcome. github.com/fallmor/poc-keycloak

So today i ll guide you how to deploy a reliable keycloak infrastructure on aws using Terraform.

Keycloak

Keycloak is an open source SSO software that permits to federate and delegate authentification. The goal of Keycloak is to provide a mechanism to log to multiple site by providing credentials for one time. Keycloak use several protocols such as OAuth, SAML2.0, and OIDC to delegate authentication to other site or social media such as facebook, github, google .... keycloak.org/about

Terraform

Terraform is one of the pioneers of Infrastructure As a Code. It use provider to provision infrastructure on cloud (aws, gcp, azure ....) Terraform guarantee consistency between runs, and provide other interesting features such as integration with version control. For example by using terraform we can have different version of our infrastructure. Terraform is the first multi-cloud immutable infrastructure tool that was introduced to the world by HashiCorp, released 7 years ago, and written in Go. terraform.io

Infrastructure on aws

Since keycloak is a security software we need to protect by restrict all unnecessary access.
For this demo i will use AWS to deploy my keycloak server. For that we need first to create a Virtual Private Cloud (VPC) with two subnets one public and one private. In the private subnet we will deployed our keycloak server. Since Administrators need sometimes to access to the keycloak server via ssh we need to set up an bastion in the public subnet.

Untitled Diagram.drawio.png

ressources deployed:

  • VPC , subnet, security groups, nat gateway, route table, route table associations

  • 2 EC2 instance

  • DNS

  • ELB

So all this configurations will be deployed using terraform. You need to have terraform installed on your computer. for the VPC our configuration will look like this.

data "aws_availability_zones" "available" {}

resource "aws_vpc" "poc_keycloak" {
  cidr_block           = var.vpc_cidr_block
  enable_dns_hostnames = true
}

resource "aws_subnet" "keycloak_public" {
  count                   = var.availability_zone_count
  cidr_block              = cidrsubnet(aws_vpc.poc_keycloak.cidr_block, 8, count.index)
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  vpc_id                  = aws_vpc.poc_keycloak.id
  map_public_ip_on_launch = true

  tags = {
    Name = "keycloak Public Subnet"
  }
}

resource "aws_subnet" "keycloak_private" {
  cidr_block        = var.private_subnet_cidr_block
  availability_zone = data.aws_availability_zones.available.names[1]
  vpc_id            = aws_vpc.poc_keycloak.id

  tags = {
    Name = "Keycloak Private Subnet"
  }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.poc_keycloak.id
}
resource "aws_eip" "nat_eip" {
  vpc = true
}

resource "aws_nat_gateway" "nat" {
  allocation_id = aws_eip.nat_eip.id
  subnet_id     = aws_subnet.keycloak_public[0].id
}
resource "aws_route_table" "public_route" {
  vpc_id = aws_vpc.poc_keycloak.id

  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.main.id
  }
  tags = {
    Name = "table routage public"
  }
}

resource "aws_route_table_association" "public" {
  count          = var.availability_zone_count
  subnet_id      = element(aws_subnet.keycloak_public.*.id, count.index)
  route_table_id = aws_route_table.public_route.id
}

resource "aws_route_table" "private_route" {
  vpc_id = aws_vpc.poc_keycloak.id

  tags = {
    Name = "table routage privé"
  }
}
resource "aws_route_table_association" "prive" {
  subnet_id      = aws_subnet.keycloak_private.id
  route_table_id = aws_route_table.private_route.id
}

resource "aws_route" "nat_route" {
  route_table_id         = aws_route_table.private_route.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_nat_gateway.nat.id
}
resource "aws_security_group" "keycloak_sg" {
  description = "The security group used by the keycloak server"

  vpc_id = aws_vpc.poc_keycloak.id

  ingress {
    protocol    = "tcp"
    from_port   = 8080
    to_port     = 8080
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    protocol    = "tcp"
    from_port   = 80
    to_port     = 80
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    protocol    = "tcp"
    from_port   = 443
    to_port     = 443
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port = 0
    to_port   = 0
    protocol  = "-1"

    cidr_blocks = [
      "0.0.0.0/0",
    ]
  }
}

resource "aws_security_group" "keycloak_bastion_sg" {
  description = "The security group used by the bastion instance"
  vpc_id      = aws_vpc.poc_keycloak.id
  ingress {
    protocol    = "tcp"
    from_port   = 22
    to_port     = 22
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    protocol    = -1
    from_port   = 0
    to_port     = 0
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_security_group" "keycloak_admin_access" {
  description = "The security group allowing SSH administrative access to the instances"
  vpc_id      = aws_vpc.poc_keycloak.id

  ingress {
    protocol        = "tcp"
    from_port       = 22
    to_port         = 22
    security_groups = [aws_security_group.keycloak_bastion_sg.id]
  }
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.0.0.0/16"]
  }

  # Allow all from private subnet
  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = [aws_subnet.keycloak_private.cidr_block]
  }
  egress {
    from_port        = 0
    to_port          = 0
    protocol         = "-1"
    cidr_blocks      = ["0.0.0.0/0"]
    ipv6_cidr_blocks = ["::/0"]
  }

}
resource "aws_security_group" "elb" {
  name        = "sec_group_elb"
  description = "Security group for public facing ELBs"
  vpc_id      = aws_vpc.poc_keycloak.id

  # HTTP access from anywhere
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # HTTPS access from anywhere
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # Outbound internet access
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

So what we have done here is :

  • we create a new VPC and their subnets (one public and one private). For each subnet we created a route table table that permits the traffic to flow. keep in mind that a "subnet is deemed to be a Public Subnet if it has a Route Table that directs traffic to the Internet Gateway". That's why we created an internet gateway and attached it to the public subnet. To permit the private subnet to have access to the internet we create a route table that associate the private subnet and the nat gateway.

In aws security group is a low level firewall that restrict access to a service. By default a newly created security will restrict all accees on all port. For security purpose we created different security group to restrict access:

  • One security group that allow admin to connect to the keycloak by ssh through the bastion
  • Two security group attached to the Elastic Loadbalancer
  • One security group for the keycloak server

The second big thing is the bastion server

resource "aws_instance" "bastion" {
  ami           = var.AMI
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.keycloak_public[0].id

  vpc_security_group_ids = [aws_security_group.keycloak_bastion_sg.id]
  key_name               = aws_key_pair.bastion_keypair.id
  # nginx installation
  provisioner "file" {
    source      = "test.sh"
    destination = "/tmp/test.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/test.sh",
      "sudo /tmp/test.sh"
    ]
  }
  connection {
    type        = "ssh"
    user        = var.EC2_USER
    private_key = file("${path.module}/test-ssh/id_rsa")
    host        = self.public_ip
  }
}
// Sends your public key to the instance
resource "aws_key_pair" "bastion_keypair" {
  key_name   = "bastion_keypair"
  public_key = file("${path.module}/test-ssh/id_rsa.pub")
}

the thing that is special about the bastion is the terraform remote_exec resource that we use to execute a bash script to install some software. we need also to generate ssh private key using : ssh-keygen to the directory test-ssh/

here we have our keycloak ressource:

resource "aws_instance" "keycloak" {
  ami           = var.AMI
  instance_type = "t2.micro"
  subnet_id     = aws_subnet.keycloak_private.id

  vpc_security_group_ids = [aws_security_group.keycloak_admin_access.id, aws_security_group.keycloak_sg.id]
  key_name               = "bastion_keypair"
  provisioner "file" {
    source      = "test.sh"
    destination = "/tmp/test.sh"
  }
  provisioner "file" {
    source      = "keycloak.yml"
    destination = "/tmp/keycloak.yml"
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/test.sh",
      "sudo /tmp/test.sh"
    ]
  }
  provisioner "remote-exec" {
    inline = [
      "nohup sudo docker-compose -f /tmp/keycloak.yml up &",
      "sleep 1"
    ]
  }
  connection {
    type                = "ssh"
    user                = var.EC2_USER
    bastion_host        = aws_instance.bastion.public_ip
    bastion_host_key    = file("${path.module}/test-ssh/id_rsa.pub")
    bastion_private_key = file("${path.module}/test-ssh/id_rsa")
    bastion_port        = 22
    bastion_user        = var.EC2_USER
    private_key         = file("${path.module}/test-ssh/id_rsa")
    host                = self.private_ip
  }
}

for the keycloak server we executed a script that install docker and docker-compose on the host. After that we use the remote provisioner to execute the docker compose command. Since remote-exec does not support well process that run on foreground we need to add sleep 1 github.com/hashicorp/terraform/issues/6229.

Capture d’écran 2021-11-23 à 14.40.57.png

for the remaining part of the terraform code you can have a look on my git repository: github.com/fallmor/poc-keycloak. I m also writing another blog post on how to configure keycloak using terraform and how to integrate it with a dynamic website.

Thanks you