Crie seu próprio gateway de acesso

Implemente seu próprio gateway sobre o OpenResty para permitir a integração de aplicativos que não são da SiteMinder, sem precisar reescrever.

5
leitura mínima

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.

Arquitetura de referência do Siteminder com gateway de acesso personalizado


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:


local _M = {
  --
  -- Function return codes
  --
  SM_AGENTAPI_NOCONNECTION                = -3,
  SM_AGENTAPI_TIMEOUT                     = -2,
  SM_AGENTAPI_FAILURE                     = -1,
  SM_AGENTAPI_SUCCESS                     = 0,
  SM_AGENTAPI_YES                         = 1,
  SM_AGENTAPI_NO                          = 2,
  SM_AGENTAPI_CHALLENGE                   = 3,
  SM_AGENTAPI_UNRESOLVED                  = 4,
  SM_GETBOOTSTRAPCONFIG_FAILURE           = 5,
  SM_AGENTAPIINIT_FAILURE                 = 6,
  SM_FETCHCONFIGDATA_FAILURE              = 7,
  SM_AGENTAPI_ERR_HCO_NOT_ENABLED         = 8,
  SM_AGENTAPI_ERR_HCO_NOT_OURS            = 9,
  SM_AGENTAPI_ERR_HCO_NOT_CHANGED         = 10,
  SM_AGENTAPI_YES_DLP                     = 11,
  SM_AGENTAPI_INVALID_ARGS                = 12,
  --
  -- Attributes
  --
  SM_AGENTAPI_ATTR_AUTH_DIR_OID           = 151,
  SM_AGENTAPI_ATTR_AUTH_DIR_NAME          = 213,
  SM_AGENTAPI_ATTR_AUTH_DIR_SERVER        = 214,
  SM_AGENTAPI_ATTR_AUTH_DIR_NAMESPACE     = 215,
  SM_AGENTAPI_ATTR_USERMSG                = 216,
  SM_AGENTAPI_ATTR_USERDN                 = 218,
  SM_AGENTAPI_ATTR_USERUNIVERSALID        = 152,
  SM_AGENTAPI_ATTR_IDENTITYSPEC           = 156,
  -- Well-known attributes used by the Single Sign-On APIs
  -- not previously defined above                 
  SM_AGENTAPI_ATTR_STARTSESSIONTIME       = 154,
  SM_AGENTAPI_ATTR_LASTSESSIONTIME        = 155,
  SM_AGENTAPI_ATTR_DEVICENAME             = 200,
  SM_AGENTAPI_ATTR_SESSIONID              = 205,
  SM_AGENTAPI_ATTR_CLIENTIP               = 208,
  SM_AGENTAPI_ATTR_SESSIONSPEC            = 209,
  SM_AGENTAPI_ATTR_USERNAME               = 210,
  SM_AGENTAPI_ATTR_IDLESESSIONTIMEOUT     = 225,
  SM_AGENTAPI_ATTR_MAXSESSIONTIMEOUT      = 226,
  SM_AGENTAPI_ATTR_SSOZONE                = 228,
  SM_AGENTAPI_ATTR_SESSION_DOMAIN         = 229,
  -- Well-known attributes used by the Authentication Chain 
  -- not previously defined above                 
  SM_AGENTAPI_ATTR_AUTHCHAINSPEC          = 240,
  SM_AGENTAPI_ATTR_CHAINREALMCREDENTIALS  = 219,
  SM_AGENTAPI_ATTR_CHAINFORMLOC           = 220,
}

local mt = {__index = _M}

ffi.cdef [[
    typedef struct Sm_AgentApi_Server_s
    {
        char    lpszIpAddr[256];       
        long    nConnMin;                                
        long    nConnMax;                                
        long    nConnStep;                               
        long    nTimeout;                                 
        long    nPort[3];                                 
        void*   pHandle[3];                               
        long    nClusterSeq;                              
    } Sm_AgentApi_Server_t;

    typedef struct Sm_AgentApi_Init_s
    {
      long    nVersion;                                 
      char    lpszHostName[256];      
      char    lpszSharedSecret[256];  
      long    nFailover;                               
      long    nNumServers;                              
      Sm_AgentApi_Server_t*    pServers;               
    } Sm_AgentApi_Init_t;

    typedef struct Sm_AgentApi_Attribute_s
    {
      long    nAttributeId;
      long    nAttributeTTL;
      long    nAttributeFlags;
      char    lpszAttributeOid[64];
      long    nAttributeLen;
      char*   lpszAttributeValue;
    } Sm_AgentApi_Attribute_t;

    typedef struct Sm_AgentApi_Session_s
    {
      int32_t nReason;
      int32_t nIdleTimeout;
      int32_t nMaxTimeout;
      int32_t nCurrentServerTime;
      int32_t nSessionStartTime;
      int32_t nSessionLastTime;
      char  lpszSessionId[64];
      char  lpszSessionSpec[2048];
    } Sm_AgentApi_Session_t;

    typedef struct Sm_AgentApi_ResourceContext_s
    {
      char    lpszAgent[256];
      char    lpszServer[256];
      char    lpszAction[256];
      char    lpszResource[8192];
    } Sm_AgentApi_ResourceContext_t;

    typedef struct Sm_AgentApi_Realm_s
    {
      char    lpszDomainOid[64];
      char    lpszRealmOid[64];
      char    lpszRealmName[256];
      long    nRealmCredentials;
      char    lpszFormLocation[8192];
    } Sm_AgentApi_Realm_t;

    typedef struct Sm_AgentApi_UserCredentials_s
    {
      int32_t nChallengeReason;
      char    lpszUsername[256];
      char    lpszPassword[4096];
      char    lpszCertUserDN[1024];
      char    lpszCertIssuerDN[1024];
      int32_t nCertBinaryLen;
      char*    lpszCertBinary;
    } Sm_AgentApi_UserCredentials_t;

    int Sm_AgentApi_GetConfig (Sm_AgentApi_Init_t* pInit, const char *lpszAgentName, const char *lpszPath);
    int Sm_AgentApi_Init (const Sm_AgentApi_Init_t* pInitStruct, void** ppHandle);
    int Sm_AgentApi_SetDefaultAgentId(const char *pszAgentIdentity, void* pHandle);
    int Sm_AgentApi_IsProtected (
      const void* pHandle,
      const char* lpszClientIpAddr,
      const Sm_AgentApi_ResourceContext_t* pResourceContext,
      Sm_AgentApi_Realm_t* pRealm);
    int Sm_AgentApi_Authorize (
      const void* pHandle,
      const char* lpszClientIpAddr,                                /* optional */
      const char* lpszTransactionId,                               /* optional */
      const Sm_AgentApi_ResourceContext_t* pResourceContext,
      const Sm_AgentApi_Realm_t* pRealm,
      Sm_AgentApi_Session_t* pSession,
      long* pNumAttributes,
      Sm_AgentApi_Attribute_t** ppAttributes);

    int Sm_AgentApi_CreateSSOToken (const void* pHandle, Sm_AgentApi_Session_t* pSession, long nNumAttributes,
      Sm_AgentApi_Attribute_t* pTokenAttributes, long* pNumSSOTokenLength , char* lpszSSOToken); 
    int Sm_AgentApi_Login (
        const void* pHandle,
        const char* lpszClientIpAddr,                                /* optional */
        const Sm_AgentApi_ResourceContext_t* pResourceContext,
        const Sm_AgentApi_Realm_t* pRealm,
        const Sm_AgentApi_UserCredentials_t* pUserCredentials,
        Sm_AgentApi_Session_t* pSession,
        long* pNumAttributes,
        Sm_AgentApi_Attribute_t** ppAttributes);
    void Sm_AgentApi_FreeAttributes (const long nNumAttributes, const Sm_AgentApi_Attribute_t* pAttributes);
    int Sm_AgentApi_DecodeSSOToken(
        const void* pHandle,
        const char* lpszSSOToken,
        long* nTokenVersion,
        long* pThirdPartyToken,
        long* pNumAttributes,
        Sm_AgentApi_Attribute_t** ppTokenAttributes,
        long nUpdateToken,
        long* pNumUpdatedSSOTokenLength,
        char* lpszUpdatedSSOToken);
]]

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.
Opa! Algo deu errado.