En el post anterior, analizamos cómo la contenedorización facilita la transición de los sistemas de identidad heredados, lo que nos permite crear entornos de forma fácil y rápida que puedan ejecutarse sin problemas en Amazon EC2, Google Cloud, Azure, Virtualbox y otros. Mostramos cómo funciona esto específicamente, con CA Siteminder. Más allá de eso, en este artículo exploraremos una solución sin necesidad de código para incorporar aplicaciones que no sean de Siteminder.
El SSO en la nube basado en estándares puede ayudar, pero...
Cuando «levantamos y cambiamos» un sistema de identidad heredado, el entorno de nube alojará nuevas aplicaciones y servicios que no formaban parte de la configuración anterior. Habilitar el inicio de sesión único (SSO) para las aplicaciones web más modernas que cuentan con SAML u OpenID Connect (OIDC) debería ser bastante sencillo, ya que Siteminder funciona perfectamente con ambos estándares.
En este escenario, Siteminder desempeñará el papel de proveedor de identidad (IdP) y la aplicación asumirá el papel de proveedor de servicios (SP). De esta forma, podemos evitar el esfuerzo de reescribir la aplicación y, al mismo tiempo, extender el inicio de sesión único a cualquier aplicación nueva. Esto significa que el usuario no tiene que volver a introducir sus credenciales al intentar acceder a ninguna de las nuevas aplicaciones o servicios. Hemos obtenido dos ventajas empresariales: un menor esfuerzo y una mejor experiencia de usuario.
Cuando esto se queda corto
De forma predeterminada, Siteminder protege las aplicaciones mediante una pasarela de acceso, también conocida como servidor proxy seguro (SPS). En pocas palabras, el Access Gateway intercepta las solicitudes, autentica y autoriza a los usuarios antes de que se les conceda el acceso a la aplicación de destino. La aplicación no conoce Siteminder, ya que se basa en los encabezados HTTP estándar empleados por Access Gateway para identificar al usuario, así como en varias otras afirmaciones.
Idealmente, esto debería servir como una solución alternativa, con el objetivo de minimizar las interrupciones y, al mismo tiempo, abordar las tareas de migración a más largo plazo. La desventaja es que la deuda técnica (en lo que respecta a la modernización de la IAM) aumentará, ya que el legado del que se está intentando dejar atrás ahora está haciendo más que antes. Además, como parte del nuevo ecosistema, es muy probable que haya aplicaciones que no admitan el SSO basado en estándares (es decir, no SAML ni OIDC) o que no estén basadas en la web (por ejemplo, las aplicaciones de Windows que realizan la autenticación de Active Directory). Por último, el entorno heredado basado en la nube aumentará los costos operativos debido al mantenimiento.
Los riesgos en los que se incurre pueden incluir:
* Aumento de la deuda técnica * Pérdida de soporte para el SSO basado en estándares * Aumento de los gastos operativos
¿Cómo podemos reducir al mínimo las interrupciones y, al mismo tiempo, reducir la deuda técnica de nuestra implementación de IAM?
Asumir la titularidad del «pegamento identitario»
Vale la pena señalar que Access Gateway es esencialmente un proxy inverso creado en torno a un servidor web Apache, configurado con un conjunto de módulos específicos de Siteminder. En realidad, el núcleo interno se encuentra en esos complementos, que se encargan de la autenticación y la autorización en nombre de las aplicaciones posteriores.
En los últimos años, el servidor web Nginx ha ido ganando popularidad de manera constante debido a su naturaleza liviana y su compatibilidad con la arquitectura de microservicios. Por lo tanto, es probable que Nginx forme parte de su arquitectura en la nube. Dado que es funcionalmente equivalente y brilla en términos de extensibilidad, ¿por qué no implementar además nuestra propia pasarela de acceso?
La buena noticia es que puedes extender Nginx hasta el núcleo sin la carga adicional de escribir un complemento. Simplemente puede implementar lo que desee, utilizando scripts de Lua simples y sencillos. El nombre de esta combinación de Nginx y Lua (más precisamente Luajit) se llama OpenResty. Es de código abierto y tiene una comunidad enorme.
Pero, ¿cómo se espera que Nginx se comunique con Siteminder (más específicamente, el Policy Server)? FFI es tu amigo aquí. Según Wikipedia:
UN interfaz de función externa (FFI) es un mecanismo mediante el cual un programa escrito en una lenguaje de programación puede llamar a rutinas o hacer uso de servicios escritos en otra.
En términos sencillos, lo que esto significa para nuestro escenario es lo siguiente: podemos invocar el SDK del agente de Siteminder desde LuaJit, lo que nos permite hacernos pasar por un agente de Siteminder en sentido ascendente, lo que protege las aplicaciones en sentido descendente, tal como lo hacía la pasarela de acceso. Para hacer cumplir las reglas de proxy de las pasarelas de acceso, solo tenemos que seguir la ruta programática: escribir las condiciones de Lua if-then else correspondientes.
Adiós, Access Gateway, archivos XML, ProxyUI y amigos: ahora solo tenemos el proxy inverso basado en OpenREST y el Policy Server. Las aplicaciones ni siquiera notarán el cambio, ya que se cumple el contrato habitual, es decir, mediante encabezados HTTP.
Por último, pero no por ello menos importante, una vez que Lua interactúe con Siteminder, podremos volver a utilizar la misma implementación para proteger no solo las aplicaciones web, sino también las que no tengan acceso directo. No es necesario volver a implementar todo en Perl.
Caso práctico: generar un token SSO válido de Siteminder
Configure su entorno de desarrollo
Para crear un entorno portátil, recomendamos utilizar un enfoque basado en contenedores. En este caso, usaremos Docker. Este es un ejemplo de dockerfile que define una imagen que contiene un entorno de desarrollo de Siteminder:
FROM openresty/openresty:centos
ENV PS_ZIP=ps-12.8-sp05-linux-x86-64.zip \
SDK_ZIP=smsdk-12.8-sp05-linux-x86-64.zip \
BASE_DIR=/opt/CA/siteminder \
INSTALL_TEMP=/tmp/sm_temp
ENV SCRIPT_DIR=${INSTALL_TEMP}/dockertools
#
# Creation of User, Directories and Installation of OS packages
# ----------------------------------------------------------------
RUN dnf config-manager --set-enabled powertools
RUN yum install -y which unzip rng-tools java-1.8.0-openjdk-1:1.8.0.292.b10-1.el8_4.x86_64 \
ksh openldap-clients openssh-server xauth libnsl gcc gcc-c++ openmotif
RUN groupadd smuser && \
useradd smuser -g smuser
RUN mkdir -p ${BASE_DIR} && \
chmod a+xr ${BASE_DIR} && \
chown smuser:smuser ${BASE_DIR}
RUN mkdir -p ${INSTALL_TEMP} && \
chmod a+xr ${INSTALL_TEMP} && chown smuser:smuser ${INSTALL_TEMP}
# Increase entropy
# ----------------
RUN mv /dev/random /dev/random.org && \
ln -s /dev/urandom /dev/random
# Copy packages and scripts
# -------------------------
COPY --chown=smuser:smuser install/* ${INSTALL_TEMP}/
COPY --chown=smuser:smuser ca-ps-installer.properties ${INSTALL_TEMP}/
COPY --chown=smuser:smuser sdk-installer.properties ${INSTALL_TEMP}/
# Install Policy Server
# -------------------------
RUN unzip ${INSTALL_TEMP}/${PS_ZIP} -d ${INSTALL_TEMP} && \
chmod +x ${INSTALL_TEMP}/ca-ps-12.8-sp05-linux-x86-64.bin && \
${INSTALL_TEMP}/ca-ps-12.8-sp05-linux-x86-64.bin -i silent -f ${INSTALL_TEMP}/ca-ps-installer.properties
RUN echo ". /opt/CA/siteminder/ca_ps_env.ksh" >> /home/smuser/.bash_profile
# Install the SDK
# -----------------------------------------------
RUN unzip ${INSTALL_TEMP}/${SDK_ZIP} -d ${INSTALL_TEMP} && \
chmod +x ${INSTALL_TEMP}/ca-sdk-12.8-sp05-linux-x86-64.bin && \
${INSTALL_TEMP}/ca-sdk-12.8-sp05-linux-x86-64.bin -i silent -f ${INSTALL_TEMP}/sdk-installer.properties
USER smuser
# Define default command to start bash.
ENTRYPOINT ["/bin/bash"]
La imagen generada se basará en la imagen de OpenResty de CentOS en lugar de en la imagen de stock, por lo que solo necesitamos incluir los paquetes específicos de Siteminder en la imagen. Nota: Estamos instalando el paquete Policy Server, pero no vamos a continuar con la fase de configuración. Esto se debe a que solo necesitamos las bibliotecas dinámicas que proporciona, así como las herramientas, para conectarnos con un Policy Server externo y en funcionamiento.
Cree la imagen con el siguiente comando:
$ docker build -t mysmdev .
Luego ejecútelo usando:
$ docker run -t -i mysmdev
Para poder conectarnos al Policy Server, necesitamos generar un descriptor SMHost.conf y hacer referencia a él desde el script Lua.
Agrupe los stubs FFI de LuaJit para la API del agente
Definamos primero nuestros tipos de FFI para la API del agente:
Aunque se parecen a las declaraciones que normalmente se encuentran en los archivos de encabezado en «C», en este contexto se utilizan para los scripts de Lua, para poder definir la forma de los datos que se intercambiarán con la biblioteca dinámica del agente.
A continuación, declaramos las funciones del agente que funcionan en estos tipos:
function _M.getconfig(self, smhostpath)
local agentapi = ffi.new("Sm_AgentApi_Init_t")
smagentapilib.Sm_AgentApi_GetConfig(agentapi, nil, smhostpath)
return agentapi
end
function _M.init(agentapi, phandle)
return smagentapilib.Sm_AgentApi_Init(agentapi, phandle)
end
function _M.boot(smhostpath, agentname)
local pSmApiHandle = ffi.new("void*[1]")
local agentapi = _M.getconfig(smhostpath)
_M.init(agentapi, pSmApiHandle)
if smagentapilib.Sm_AgentApi_SetDefaultAgentId(agentname, pSmApiHandle[0]) ==
_M.SM_AGENTAPI_FAILURE then return -1 end
return pSmApiHandle[0]
end
function _M.is_protected(file, method, pSmApiHandle)
local resourceContext = ffi.new("Sm_AgentApi_ResourceContext_t")
local realm = ffi.new("Sm_AgentApi_Realm_t")
ffi.copy(resourceContext.lpszResource, file)
ffi.copy(resourceContext.lpszAction, method)
ffi.copy(resourceContext.lpszServer, "extapp")
local res = smagentapilib.Sm_AgentApi_IsProtected(pSmApiHandle, "127.0.0.1",
resourceContext, realm)
return res, resourceContext, realm
end
function _M.login(loginName, password, resourceContext, realm, pSmApiHandle)
local userCreds = ffi.new("Sm_AgentApi_UserCredentials_t")
local session = ffi.new("Sm_AgentApi_Session_t")
local iNumAttributes = ffi.new("long[1]", 0)
local pAttributes = ffi.new("Sm_AgentApi_Attribute_t*[1]")
ffi.copy(userCreds.lpszUsername, loginName)
ffi.copy(userCreds.lpszPassword, password)
local res = smagentapilib.Sm_AgentApi_Login(pSmApiHandle, "127.0.0.1",
resourceContext, realm,
userCreds, session,
iNumAttributes, pAttributes)
return res, session, iNumAttributes, pAttributes
end
function _M.sso(pSmApiHandle, pszUserDN, session, numAttributes)
if pSmApiHandle == nil then return -1 end
local pAttr = ffi.new("Sm_AgentApi_Attribute_t[3]")
local pszIPAddress = "123.45.67.89"
local pszSSOToken = ffi.new("char[2048]")
local nTlen = ffi.new("long[1]", 2048)
local result = 0
local attrCount = ffi.new("long", 1)
ffi.fill(pszSSOToken, ffi.sizeof("char[2048]"))
pAttr[0].nAttributeId = _M.SM_AGENTAPI_ATTR_USERDN
pAttr[0].lpszAttributeValue = ffi.new("char[?]", string.len(pszUserDN) + 1) -- new char[strlen(pszUserDN)+1];
ffi.copy(pAttr[0].lpszAttributeValue, pszUserDN)
local res = smagentapilib.Sm_AgentApi_CreateSSOToken(pSmApiHandle, session,
numAttributes, pAttr,
nTlen, pszSSOToken)
return res, ffi.string(pszSSOToken)
end
function _M.decode_sso_token(pSmApiHandle, ssoToken)
local nTokenVer = ffi.new("long[1]", 0)
local nNumDecAttr = ffi.new("long[1]", 0)
local nNumThdParty = ffi.new("long[1]", 0)
local nNumUpdateToken = 1
local nNumUpdateTokenLength = ffi.new("long[1]", 2048)
local pszUpdatedSSOToken = ffi.new("char[2048]")
local pTokenAttributes = ffi.new("Sm_AgentApi_Attribute_t*[1]")
if pSmApiHandle == nil then return -1 end
local result = smagentapilib.Sm_AgentApi_DecodeSSOToken(pSmApiHandle,
ssoToken, nTokenVer,
nNumThdParty,
nNumDecAttr,
pTokenAttributes,
nNumUpdateToken,
nNumUpdateTokenLength,
pszUpdatedSSOToken)
local tokenAttrs = {}
for i = 1, tonumber(nNumDecAttr[0]) do
local pTemp = pTokenAttributes[0][i - 1]
tokenAttrs[tonumber(pTemp.nAttributeId)] = tostring(ffi.string(pTemp.lpszAttributeValue))
end
smagentapilib.Sm_AgentApi_FreeAttributes(nNumDecAttr[0], pTokenAttributes[0]);
return result, nNumDecAttr, tokenAttrs, ffi.string(pszUpdatedSSOToken)
end
return _M
Al igual que los tipos declarados anteriormente, también asignan una a una las funciones de la biblioteca del agente.
Invocar al agente desde OpenResty
Una característica interesante de OpenResty es que el código se puede ejecutar sin memoria, ya sea desde la línea de comandos o desde su suite de pruebas. Esto permite una respuesta rápida a la hora de implementar un comportamiento que no requiere ningún procesamiento. Una vez que hayas desarrollado tu biblioteca, puedes consumirla tal cual desde tu interfaz de usuario.
local agent = require "resty.siteminder.agent"
local smhost = arg[1]
local agentname = arg[2]
local pSmApiHandle = agent.boot(smhost, agentname)
if pSmApiHandle ~= -1 then
local _, resourceContext, realm = agent.is_protected("/private/index.html",
"GET", pSmApiHandle)
local res, session, iNumAttributes, pAttributes =
agent.login("admin", "secret", resourceContext, realm, pSmApiHandle)
local res2, token = agent.sso(pSmApiHandle,
"cn=admin,ou=Contoso,o=psdsa,c=US", session,
iNumAttributes[0])
print("Token: " .. token)
local res3, attrsnum, tokenAttrs, updatedToken =
agent.decode_sso_token(pSmApiHandle, token)
print("User DN: " .. tokenAttrs[agent.SM_AGENTAPI_ATTR_USERDN])
for akey, aval in pairs(tokenAttrs) do
if #aval > 0 then
print(akey .. "=" .. aval)
end
end
end
Antes de ejecutar el script, asegúrese de haber configurado el dominio correspondiente en el servidor de políticas. Por ejemplo, "/private/index.html" es uno de los recursos protegidos.
Creación e implementación
Es posible que desee utilizar el siguiente Makefile para simplificar la construcción y el despliegue de nuestros scripts.
OPENRESTY_PREFIX=/usr/local/openresty
LUA_VERSION := 5.3
PREFIX ?= /usr/local
LUA_INCLUDE_DIR ?= $(PREFIX)/include
LUA_LIB_DIR ?= $(PREFIX)/lib/lua/$(LUA_VERSION)
INSTALL ?= install
.PHONY: all test install
all: ;
install: all
$(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty/siteminder
$(INSTALL) lib/resty/siteminder*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/
$(INSTALL) lib/resty/siteminder/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/siteminder/
example: install
resty -I /usr/local/lib/lua/5.3 examples/sso.lua conf/SmHost.conf spsapacheagent
test: all
PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t
Prueba
Ejecuta el siguiente código desde tu shell:
$ make example
Debe imprimir el DN del usuario y el token de SSO generado.
Conclusiones
Fue todo un viaje. Hemos demostrado cómo se puede aprovechar el SDK de Siteminder para crear tu propia pasarela de acceso sobre software libre, como el moderno servidor web de Nginx, sin tener que reinventar nada, y utilizar scripts de Lua en lugar de tener que lidiar con todos los matices del código nativo, como la gestión manual de la memoria.