Na post anterior, discutimos como a conteinerização facilita a transição de sistemas de identidade legados, permitindo a criação fácil e rápida de ambientes que podem ser executados sem problemas no Amazon EC2, Google Cloud, Azure, Virtualbox e outros. Mostramos especificamente como isso funciona com o CA Siteminder. Além disso, neste post, exploraremos uma solução sem código para a integração de aplicativos que não são da SiteMinder.
O Cloud SSO baseado em padrões pode ajudar, mas...
Quando “elevamos e mudamos” um sistema de identidade legado, o ambiente de nuvem hospeda novos aplicativos e serviços que não faziam parte da configuração anterior. Habilitar o login único (SSO) para os aplicativos web mais modernos que apresentam SAML ou OpenID Connect (OIDC) deve ser bem simples, já que o Siteminder funciona bem com esses dois padrões.
Nesse cenário, o Siteminder desempenhará o papel de provedor de identidade (IdP) e o aplicativo assumirá o papel de provedor de serviços (SP). Dessa forma, podemos evitar o esforço de reescrever aplicativos e, ao mesmo tempo, estender o SSO a qualquer novo aplicativo. Isso significa que o usuário não precisa inserir novamente suas credenciais ao tentar acessar qualquer um dos novos aplicativos ou serviços. Obtivemos duas vantagens corporativas: redução do esforço e melhoria da experiência do usuário.
Quando isso é insuficiente
Por padrão, a forma como o Siteminder protege os aplicativos é confrontá-los com um Access Gateway, também conhecido como Secure Proxy Server (SPS). Resumindo, o Access Gateway intercepta solicitações, autenticando e autorizando usuários antes que eles tenham acesso ao aplicativo de destino. O aplicativo não conhece o Siteminder, pois ele se baseia nos cabeçalhos HTTP padrão empregados pelo Access Gateway para identificar o usuário, além de várias outras reivindicações.
Idealmente, isso deve servir como uma solução alternativa, com o objetivo de minimizar as interrupções e, ao mesmo tempo, abordar tarefas de migração de longo prazo. A desvantagem é que a dívida técnica — no que diz respeito à modernização do IAM — aumentará, já que o legado do qual você está tentando migrar agora está fazendo mais do que antes. Além disso, como parte do novo ecossistema, é muito provável que haja aplicativos que não oferecem suporte a SSO baseado em padrões (portanto, sem SAML ou OIDC) ou que não sejam baseados na Web (por exemplo, aplicativos do Windows executando a autenticação do Active Directory). Finalmente, o ambiente legado baseado em nuvem aumentará os custos operacionais devido à manutenção.
Os riscos incorridos podem incluir:
* Aumento da dívida técnica * Perda de suporte para SSO baseado em padrões * Aumento da despesa operacional
Como podemos reduzir ao mínimo a interrupção e, ao mesmo tempo, diminuir a dívida técnica de nossa implantação do IAM?
Assumindo a propriedade da “cola de identidade”
É importante notar que o Access Gateway é essencialmente um proxy reverso construído em torno de um servidor web Apache, configurado com um conjunto de módulos específicos do SiteMinder. O núcleo interno está realmente dentro desses plug-ins, que se encarregam de fazer autenticação e autorização em nome dos aplicativos downstream.
Nos últimos anos, o servidor web Nginx vem ganhando popularidade constantemente devido à sua natureza leve e compatibilidade com a arquitetura de microsserviços. Portanto, é provável que o Nginx faça parte da sua arquitetura em nuvem. Dado que é funcionalmente equivalente e brilha em termos de extensibilidade, por que não implementar nosso próprio gateway de acesso em cima dele?
A boa notícia é que você pode estender o Nginx até o núcleo sem a carga adicional de escrever um plugin. Você pode simplesmente implementar o que quiser, usando scripts Lua simples e simples. O nome dessa combinação entre Nginx e Lua, mais precisamente Luajit, é chamado de OpenResty. É de código aberto e tem uma grande comunidade.
Mas como se espera que o Nginx fale com o Siteminder (mais especificamente, com o servidor de políticas)? FFI é sua amiga aqui. De acordo com a Wikipedia:
UM interface de função externa (FFI) é um mecanismo pelo qual um programa escrito em um linguagem de programação pode chamar rotinas ou fazer uso de serviços escritos em outra.
Em termos leigos, o que isso significa para o nosso cenário é o seguinte: podemos invocar o Siteminder Agent SDK da LuaJit, permitindo que nos façamos passar por um agente Siteminder upstream, protegendo aplicativos downstream, assim como o gateway de acesso fazia. Para impor as regras de proxy do gateway de acesso, podemos simplesmente seguir a rota programática: escrever as condições correspondentes de Lua if-then-else.
Adeus, Access Gateway, arquivos XML, ProxyUI e amigos: agora é só o proxy reverso baseado em OpenResty e o Policy Server. Os aplicativos nem perceberão a mudança, já que o contrato usual, ou seja, por meio de cabeçalhos HTTP, é honrado.
Por último, mas não menos importante, quando tivermos Lua interagindo com o Siteminder, podemos reutilizar a mesma implementação para proteger não apenas aplicativos web, mas também aplicativos sem interface. Não há necessidade de reimplementar tudo em Perl.
Estudo de caso: Gere um token SSO válido do Siteminder
Configure seu ambiente de desenvolvimento
Para criar um ambiente portátil, recomendamos usar uma abordagem baseada em contêineres. Nesse caso, usaremos o Docker. Aqui está um exemplo de dockerfile que define uma imagem contendo um ambiente de desenvolvimento do 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"]
A imagem gerada será baseada na imagem do OpenResty CentOS em vez da imagem de estoque, então só precisamos agrupar os pacotes específicos do SiteMinder na imagem. Nota: Estamos instalando o pacote do Policy Server, mas não vamos prosseguir com a fase de configuração. Isso ocorre porque precisamos apenas das bibliotecas dinâmicas que ela fornece, bem como das ferramentas, para nos conectarmos a um servidor de políticas externo e funcional.
Crie a imagem usando o seguinte comando:
$ docker build -t mysmdev .
Em seguida, execute-o usando:
$ docker run -t -i mysmdev
Para podermos nos conectar ao servidor de políticas, precisamos gerar um descritor SMHost.conf e referenciá-lo a partir do script Lua.
Agrupe os stubs LuaJit FFI para a API do agente
Vamos primeiro definir nossos tipos de FFI para a API do agente:
Embora se assemelhem a declarações normalmente encontradas em arquivos de cabeçalho 'C', nesse contexto elas são usadas para scripts Lua, para poder definir a forma dos dados a serem trocados com a biblioteca dinâmica do Agente.
Em seguida, declaramos funções de agente que operam nesses 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
Assim como os tipos declarados anteriormente, eles também mapeiam individualmente as funções da biblioteca Agent.
Invoque o agente do OpenResty
Um recurso interessante do OpenResty é que o código pode ser executado sem cabeçalho, seja na linha de comando ou em sua suíte de testes. Isso permite uma resposta rápida ao implementar um comportamento que não exige nenhuma renderização. Depois de aprimorar sua biblioteca, você poderá consumir como está em sua interface de usuário.
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 executar o script, verifique se você configurou o reino correspondente no servidor de políticas. Por exemplo, "/private/index.html" é um dos recursos protegidos.
Crie e implante
Talvez você queira usar o seguinte Makefile para simplificar a construção e a implantação de nossos 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
Teste
Execute o seguinte código a partir do seu shell:
$ make example
Ele deve imprimir o DN do usuário e o token SSO gerado.
Conclusões
Foi uma jornada e tanto. Mostramos como o SDK do Siteminder pode ser aproveitado para criar seu próprio gateway de acesso com base no software livre, como o moderno servidor web Nginx, sem reinventar a roda; e usando scripts Lua em vez de ter que lidar com todas as nuances do código nativo, como o gerenciamento manual de memória.
Assine nosso boletim informativo agora!
Obrigado por se juntar ao nosso boletim informativo.