hashicorp / terraform-provider-azuread

Terraform provider for Azure Active Directory
https://registry.terraform.io/providers/hashicorp/azuread/latest/docs
Mozilla Public License 2.0
431 stars 298 forks source link

Create a SSO SAML Signing Certificate #823

Open sseekamp0 opened 2 years ago

sseekamp0 commented 2 years ago

Community Note

Description

I would like to open a new feature request to enable creation of a SSO Signing Certificate. This functionality currently exists via the API, but seems to be missing in the terraform provider.

API Reference

Azure AD service principle certificate only provides importing a certificate. This request would be for creating a SSO certificate.

New or Affected Resource(s)

References

rajish commented 2 years ago

It is possible to create the certificate, but apparently, the activation has to be done manually or using Azure CLI. Also for some unknown reason the azuread_service_principal.this.saml_metadata_url is always null when trying to read it.

My code so far:

data "azuread_client_config" "current" {}

resource "random_uuid" "oauth2_permission_scope" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "random_uuid" "app_role_user" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "random_uuid" "app_role_msiam_access" {
  keepers = {
    domain_name = var.saml_identifier_uri
  }
}

resource "azuread_application" "this" {
  display_name = var.display_name
  owners       = [data.azuread_client_config.current.object_id]
  identifier_uris = [var.saml_identifier_uri]
  api {
    oauth2_permission_scope {
      id = random_uuid.oauth2_permission_scope.id
      admin_consent_description = "Allow the application to access ${var.display_name} on behalf of the signed-in user."
      admin_consent_display_name = "Access ${var.display_name}"
      user_consent_description = "Allow the application to access ${var.display_name} on behalf of the signed-in user."
      user_consent_display_name = "Access ${var.display_name}"
      enabled = true
      type = "User"
      value = "user_impersonation"
    }
  }

  app_role {
    allowed_member_types = ["User"]
    description          = "User"
    display_name         = "User"
    id                   = random_uuid.app_role_user.id
  }
  app_role {
    allowed_member_types = ["User"]
    description          = "msiam_access"
    display_name         = "msiam_access"
    id                   = random_uuid.app_role_msiam_access.id
  }

  optional_claims {
    saml2_token {
      essential = true
      name = "email"
      additional_properties = ["sam_account_name"]
    }
  }

  web {
    homepage_url = "https://${var.app_fqdn}"
    logout_url = "https://${var.app_fqdn}"
    redirect_uris = [
      "https://${var.app_fqdn}/saml/acs"
    ]

    implicit_grant {
      access_token_issuance_enabled = false
      id_token_issuance_enabled     = true
    }
  }
}

resource "azuread_service_principal" "this" {
  application_id               = azuread_application.this.application_id
  app_role_assignment_required = false
  owners                       = [data.azuread_client_config.current.object_id]

  feature_tags {
    enterprise = true
    gallery = true
    custom_single_sign_on = true
  }

  preferred_single_sign_on_mode = "saml"

  notification_email_addresses  = var.notification_emails
}

resource "tls_private_key" "this" {
  algorithm = "RSA"
  rsa_bits  = 2048
}

resource "tls_self_signed_cert" "this" {
  allowed_uses          = ["client_auth", "server_auth"]
  key_algorithm         = "RSA"
  private_key_pem       = tls_private_key.this.private_key_pem
  validity_period_hours = 4321
  subject {
    common_name = azuread_application.this.display_name
    organization = var.organisation_name
  }
}

resource "azuread_service_principal_certificate" "this" {
  service_principal_id = azuread_service_principal.this.id
  type                  = "AsymmetricX509Cert"
  value                 = tls_self_signed_cert.this.cert_pem
  end_date_relative     = "4320h"
}

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo '\n\n-= Please activate the SSO certificate  THEN RUN `touch /tmp/ididit`; I WILL WAIT HERE =-\n\n'; while ! test -f /tmp/ididit; do sleep 1; done"
  }
  depends_on = [azuread_service_principal_certificate.this]
}

resource "azuread_claims_mapping_policy" "this" {
  definition   = [
    jsonencode(
      {
        ClaimsMappingPolicy = {
          ClaimsSchema = [
            {
              ID            = "employeeid"
              JwtClaimType  = "name"
              SamlClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"
              Source        = "user"
            },
            {
              ID            = "mail"
              JwtClaimType  = "mail"
              SamlClaimType = "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress"
              Source        = "user"
            },
            {
              ID            = "groups"
              JwtClaimType  = "groups"
              SamlClaimType = "http://schemas.microsoft.com/ws/2008/06/identity/claims/groups"
              source        = "user"
            }
          ]
          IncludeBasicClaimSet = "true"
          Version              = 1
        }
      }
    ),
  ]
  display_name = "${var.display_name}_cmp"
}

resource "azuread_service_principal_claims_mapping_policy_assignment" "this" {
  claims_mapping_policy_id = azuread_claims_mapping_policy.this.id
  service_principal_id     = azuread_service_principal.this.id
}

data "azuread_service_principal" "this" {
  object_id = azuread_service_principal.this.id
}

data "http" "idp_metadata" {
  url = data.azuread_service_principal.this.saml_metadata_url
  request_headers = {
    Accept = "application/xml"
  }
  depends_on = [
    azuread_service_principal.this,
    azuread_service_principal_certificate.this
  ]
}
NEViLLLLL commented 2 years ago

I tried the certificate from the computer and generated with tls_self_signed_cert. It really needs to be activated manually on the SSO page. But it does not show up in "Federation Metadata". If you manually import the PFX certificate, it is displayed in "Federation Metadata".

I tried @rajish code. The certificate did not show up in "Federation Metadata"

rajish commented 2 years ago

Regarding the metadata link, there's a workaround that conforms the Azure Graph documentation:

data "http" "idp_metadata" {
  url = "https://login.microsoftonline.com/${data.azuread_client_config.current.tenant_id}/federationmetadata/2007-06/federationmetadata.xml?appid=${azuread_application.this.application_id}"
  request_headers = {
    Accept = "application/xml"
  }
  depends_on = [
    azuread_service_principal.this,
    azuread_service_principal_certificate.this
  ]
}

But the certificate activation is a pain for two reasons, see the commented out code:

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo '\n\n-= Please activate the SSO certificate  THEN RUN `touch /tmp/ididit`; I WILL WAIT HERE =-\n\n'; while ! test -f /tmp/ididit; do sleep 1; done"
    # TODO no thumbprint here
    # command = "az ad sp update --id ${azuread_application.this.application_id} --set preferredTokenSigningKeyThumbprint=${tls_self_signed_cert.this.thumbprint}"
  }
  depends_on = [azuread_service_principal_certificate.this]
}
  1. There's no way to retrieve the thumprint from the certificate.
  2. Even when I tried running the command in a terminal with manually pasted values I get an error:
This command or command group has been migrated to Microsoft Graph API. Please carefully review all breaking changes introduced during this migration: https://docs.microsoft.com/cli/azure/microsoft-graph-migration
The command failed with an unexpected error. Here is the traceback:
'GraphClient' object has no attribute 'service_principals'
Traceback (most recent call last):
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/knack/cli.py", line 231, in invoke
    cmd_result = self.invocation.execute(args)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 663, in execute
    raise ex
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 726, in _run_jobs_serially
    results.append(self._run_job(expanded_arg, cmd_copy))
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 718, in _run_job
    return cmd_copy.exception_handler(ex)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/commands.py", line 54, in graph_err_handler
    raise ex
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 697, in _run_job
    result = cmd_copy(params)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 333, in __call__
    return self.handler(*args, **kwargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/command_operation.py", line 240, in handler
    result = cached_put(self.cmd, setter, **setterargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 452, in cached_put
    return _put_operation()
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 446, in _put_operation
    result = operation(**kwargs)
  File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/custom.py", line 988, in patch_service_principal
    object_id = _resolve_service_principal(graph_client.service_principals, identifier)
AttributeError: 'GraphClient' object has no attribute 'service_principals'
To open an issue, please run: 'az feedback'
brodster2 commented 2 years ago

@rajish, not sure if this is a recent change from Microsoft but when I call this endpoint, and point to an existing service principal I've created with Terraform (that doesn't have a cert) it generates a certificate and activates it. Not sure if this is new or just the way my app registration and service principal are configured? the script I used to call the endpoint https://gist.github.com/brodster2/16dfc11cdb55e4a84e3903dfab9f4bf4

dcopestake commented 2 years ago

I tried the certificate from the computer and generated with tls_self_signed_cert. It really needs to be activated manually on the SSO page. But it does not show up in "Federation Metadata". If you manually import the PFX certificate, it is displayed in "Federation Metadata".

I tried @rajish code. The certificate did not show up in "Federation Metadata"

I'm actually having exactly the same issue, did you ever find a resolution?

Basically if I provide my own cert via a azuread_service_principal_certificate resource for some reason it just doesn't show up in the App Federation Metadata, but if I deactivate and add a new one that's been automatically generated via the Azure Portal that cert does show up?!

vschum commented 2 years ago

Using the snippet from @rajish I was able to activate a token by using openssl to generate the thumbprint.

resource "null_resource" "manual_certificate_approve" {
  provisioner "local-exec" {
    command = "echo \"${tls_self_signed_cert.example.cert_pem}\" > /tmp/${tls_self_signed_cert.example.id}.pem"
    interpreter = ["/bin/bash", "-c"]
  }
  provisioner "local-exec" {
    command = "az ad sp update --id ${azuread_application.example.application_id} --set preferredTokenSigningKeyThumbprint=$(openssl x509 -in /tmp/${tls_self_signed_cert.example.id}.pem -noout -fingerprint | grep -oE '[:0-9A-F]{59}' | sed -e 's/://g')"
  }
  provisioner "local-exec" {
    command = "rm -rf /tmp/${tls_self_signed_cert.example.id}.pem"
    interpreter = ["/bin/bash", "-c"]
  }
  depends_on = [azuread_service_principal_certificate.example]
}
Annihilatopia commented 2 years ago

Hey @vschum which az cli version did you run the exec command on? Doesn't seem to work on

{
  "azure-cli": "2.39.0",
  "azure-cli-core": "2.39.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
vschum commented 2 years ago

Worked for me with the following version.

{
  "azure-cli": "2.38.0",
  "azure-cli-core": "2.38.0",
  "azure-cli-telemetry": "1.0.6",
  "extensions": {}
}
Annihilatopia commented 2 years ago

That is strange, I've downgraded az cli to 2.38.0 and I'm still getting a empty list response when trying to update preferredTokenSigningKeyThumbprint.

Couldn't find 'preferredTokenSigningKeyThumbprint=<REDACTED>' in ''. Available options: []
sherifkayad commented 2 years ago

@vschum I tried your solution and still the cert needed to be activated manually ,, does anyone have another solution maybe?

anwickes commented 2 years ago

"az ad sp update --id ${azuread_application.example.application_id} --set preferredTokenSigningKeyThumbprint=$(openssl x509 -in /tmp/${tls_self_signed_cert.example.id}.pem -noout -fingerprint | grep -oE '[:0-9A-F]{59}' | sed -e 's/://g')"

i'm also getting the following error when using the above code.

The command failed with an unexpected error. Here is the traceback: 'GraphClient' object has no attribute 'service_principals' Traceback (most recent call last): File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/knack/cli.py", line 231, in invoke cmd_result = self.invocation.execute(args) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 663, in execute raise ex File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 726, in _run_jobs_serially results.append(self._run_job(expanded_arg, cmd_copy)) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 718, in _run_job return cmd_copy.exception_handler(ex) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/commands.py", line 54, in graph_err_handler raise ex File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 697, in _run_job result = cmd_copy(params) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 333, in __call__ return self.handler(*args, **kwargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/command_operation.py", line 240, in handler result = cached_put(self.cmd, setter, **setterargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 452, in cached_put return _put_operation() File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/core/commands/__init__.py", line 446, in _put_operation result = operation(**kwargs) File "/usr/local/Cellar/azure-cli/2.37.0/libexec/lib/python3.10/site-packages/azure/cli/command_modules/role/custom.py", line 988, in patch_service_principal object_id = _resolve_service_principal(graph_client.service_principals, identifier) AttributeError: 'GraphClient' object has no attribute 'service_principals'

anwickes commented 2 years ago

This is working for me. Be interested to see if others are able to use it too.

resource "azuread_service_principal_certificate" "cert" {
  service_principal_id          = azuread_service_principal.app.id
  type                          = "AsymmetricX509Cert"
  value                         = file("sp.pem")
  end_date                      = var.sp_cert_end_date
}

resource "null_resource" "activate_saml_cert" {
  triggers = {
    value = azuread_service_principal_certificate.cert[0].value
    end_date = azuread_service_principal_certificate.cert[0].end_date
  }

  provisioner "local-exec" {
    command = <<EOT
    az login --service-principal --tenant $ARM_TENANT_ID -u $ARM_CLIENT_ID -p cert.pem --allow-no-subscriptions --output none
    thumbprint=$(openssl x509 -in sp.pem -noout -fingerprint | sed 's/SHA1 Fingerprint=//g; s/://g')
    az rest --method PATCH --uri 'https://graph.microsoft.com/v1.0/servicePrincipals/${azuread_service_principal.app.object_id}' --body "{'preferredTokenSigningKeyThumbprint':'$thumbprint'}" --headers Content-Type=application/json
    EOT
  }
}
cruikshj commented 2 years ago

This is working for me. Be interested to see if others are able to use it too.

resource "azuread_service_principal_certificate" "cert" {
  service_principal_id          = azuread_service_principal.app.id
  type                          = "AsymmetricX509Cert"
  value                         = file("sp.pem")
  end_date                      = var.sp_cert_end_date
}

resource "null_resource" "activate_saml_cert" {
  triggers = {
    value = azuread_service_principal_certificate.cert[0].value
    end_date = azuread_service_principal_certificate.cert[0].end_date
  }

  provisioner "local-exec" {
    command = <<EOT
    az login --service-principal --tenant $ARM_TENANT_ID -u $ARM_CLIENT_ID -p cert.pem --allow-no-subscriptions --output none
    thumbprint=$(openssl x509 -in sp.pem -noout -fingerprint | sed 's/SHA1 Fingerprint=//g; s/://g')
    az rest --method PATCH --uri 'https://graph.microsoft.com/v1.0/servicePrincipals/${azuread_service_principal.app.object_id}' --body "{'preferredTokenSigningKeyThumbprint':'$thumbprint'}" --headers Content-Type=application/json
    EOT
  }
}

This sort of worked for me. While it seems to have activated my cert according to the Azure Portal, the cert is still not configured completely it seems. I receive the error "AADSTS500031: Cannot find signing certificate configured." when trying to test SSO, and the metadata does not contain the cert.

cruikshj commented 2 years ago

I compared what is created by the UI and what is created by the azuread_service_principal_certificate resource. The difference is that the UI creates a certificate and adds it twice to the principal, once with the usage value of "Verify" and the other with "Sign". The resource only creates the "Verify" credential. I suspect a part of the problem is the assumption this resource provider is making here: https://github.com/hashicorp/terraform-provider-azuread/blob/cc4d2ced1633c23db9e9750f9a33903760e0928f/internal/helpers/credentials.go#L111. It is hard coded to only create credentials with "Verify" as the usage.

anwickes commented 2 years ago

Wow, great find John. Anyone with the skill to add the 2nd API call to the resource creation?

cruikshj commented 2 years ago

I wonder if it is enough to make Usage configurable on azuread_service_principal_certificate.

anwickes commented 2 years ago

Bit of a novice on how this stuff all sits together but i'm assuming you would need to issue the API call twice upon resource creation? Once for the "verify", another for "sign"?

matt-tyler commented 1 year ago

I've had a bit of poke into this - the existing resource provider for certificates is available here -

-> https://github.com/hashicorp/terraform-provider-azuread/blob/main/internal/services/serviceprincipals/service_principal_certificate_resource.go

The last update to this was on Nov 12, 2021, but the underlying client it uses to controls resource was only updated to have addTokenSigningCertificate and set the thumbprint (although Im not sure setting the thumbprint is required - i think this might be done automatically by the aforementioned API call and this is somewhat validate by @brodster2) in February. See below for the commit.

-> https://github.com/manicminer/hamilton/commit/221ac23e6a123c6edb242590f0e9150c5f01e288

The existing resource provider uses a more involved method of making direct calls to the key endpoint (which addTokenSigningCertificate effectively wraps) and then doesn't set the thumbprint.

The two approaches are actually outlined in step 4 of the below of the documentation

It should be enough to modify the resource creation routine to use addTokenSigningCertificate instead - the existing routines to may not need to change - e.g. deletion would still involve needing to look the key up by the key ID.

@manicminer I can look into prepping a PR to fix this if you would like? I'm currently fixing this in a private provider and once I've 100% confirmed this fixes behavior I can look into preparing an appropriate change?

EDIT:

This is effectively covered by the following issues & draft pull request

https://github.com/hashicorp/terraform-provider-azuread/issues/732 https://github.com/hashicorp/terraform-provider-azuread/pull/741

karol-treeline commented 1 year ago

hey hey 👋

thanks for your input, @cruikshj I followed your steps and I got the same result, my metadata file doesn't contain the certificate.

What is the status of this issue?

tagur87 commented 1 year ago

This should be closed with https://github.com/hashicorp/terraform-provider-azuread/pull/968

https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal_token_signing_certificate

jallaix commented 1 year ago

Tested and approved !

No need of https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal_certificate anymore

skgsergio commented 1 year ago

Thanks for your work @tagur87, we've started this week creating an AD App for SAML singing and we needed this.

We've done this for including the rotation, in case it is useful for someone else:

resource "time_rotating" "saml-certificate" {
  rotation_years = 3
}

resource "azuread_service_principal_token_signing_certificate" "saml-certificate" {
  service_principal_id = azuread_service_principal.app.id
  display_name         = "CN=${var.app_name} SSO Certificate"
  end_date             = time_rotating.saml-certificate.rotation_rfc3339

  provisioner "local-exec" {
    command = <<-SHELL
      az ad sp update \
        --id ${self.service_principal_id} \
        --set preferredTokenSigningKeyThumbprint=${self.thumbprint}
    SHELL
  }
}
ojc97 commented 1 year ago

I have been using the azuread_service_principal_token_signing_certificate but i cannot figure out how to set the Signing Option. Its default is Sign SAML Response, but i need to set it to Sign SAML Response and Assertion but cannot see any terraform option to do it.

Has anyone been successful with this?

Thanks

valentinahermann commented 8 months ago

any updates on this issue? We are facing the same problem. Thx.

rkosyk commented 4 months ago

@ojc97 were you able to address this?

Matioski commented 1 week ago

I have been using the azuread_service_principal_token_signing_certificate but i cannot figure out how to set the Signing Option. Its default is Sign SAML Response, but i need to set it to Sign SAML Response and Assertion but cannot see any terraform option to do it.

Has anyone been successful with this?

Thanks

Hello, I was able to address this via the tokenIssuancePolicies:

resource "azuread_service_principal_token_signing_certificate" "saml_signing_cert" {
  for_each = { for app in local.apps_map : app.app_name => app if app.type == "saml" }

  service_principal_id = azuread_service_principal.sp[each.key].id
  display_name         = "CN=${each.value.app_name} SSO Certificate"
  end_date             = time_rotating.saml_certificate.rotation_rfc3339

  provisioner "local-exec" {
    command = <<EOT
    az rest --method GET --uri https://graph.microsoft.com/v1.0/servicePrincipals/${azuread_service_principal.sp[each.key].object_id}/tokenIssuancePolicies \
    | jq -r '.value[0].id' > ./tmp/${each.key}_policy.txt
    EOT
  }

  provisioner "local-exec" {
    command = <<EOT
    az rest --method PATCH --uri https://graph.microsoft.com/v1.0/policies/tokenIssuancePolicies/$(cat ./tmp/${each.key}_policy.txt) \
    --headers 'Content-Type=application/json' \
    --body   '{
     "definition": [
        "{\n  \"TokenIssuancePolicy\": {\n    \"Version\": 1,\n    \"SigningAlgorithm\": \"http://www.w3.org/2001/04/xmldsig-more#rsa-sha256\",\n    \"TokenResponseSigningPolicy\": \"ResponseAndToken\",\n    \"SamlTokenVersion\": \"2.0\"\n  }\n}"
      ]
    }'
    EOT
  }
}

but the easiest way is actually to create the App resource using the template ID for the non-gallery apps, this will set the policy correctly automatically. The template id to set is: template_id = "8adf8e6e-67b2-4cf2-a259-e3dc5476c621"

Matioski commented 1 week ago

Thanks for your work @tagur87, we've started this week creating an AD App for SAML singing and we needed this.

We've done this for including the rotation, in case it is useful for someone else:

resource "time_rotating" "saml-certificate" {
  rotation_years = 3
}

resource "azuread_service_principal_token_signing_certificate" "saml-certificate" {
  service_principal_id = azuread_service_principal.app.id
  display_name         = "CN=${var.app_name} SSO Certificate"
  end_date             = time_rotating.saml-certificate.rotation_rfc3339

  provisioner "local-exec" {
    command = <<-SHELL
      az ad sp update \
        --id ${self.service_principal_id} \
        --set preferredTokenSigningKeyThumbprint=${self.thumbprint}
    SHELL
  }
}

For me an issue remains as this is not updating: preferredTokenSigningKeyEndDateTime. Has anyone succeeded in that? Thanks