AWS

Regole di ingresso per IP dinamici

Lettura 6 minuti

Gestione delle regole di ingress dei Security Group per consentire l’accesso ad IP dinamici.

Necessità

Le nostre infrastrutture sono progettate per restare isolate in una sottorete privata della VPC, questo per aumentare il livello di sicurezza e bloccare ogni tipo di accesso indesiderato direttamente dal layer network. Per ragioni varie di gestione, verifiche e troubleshooting abbiamo la necessità di accedere alle risorse contenute in queste infrastrutture isolate.

Istanza bastione

La soluzione comune per questo tipo di setup è l’utilizzo di un bastione, ovvero un’istanza EC2 esposta al mondo che consente di accedere alle altre risorse isolate. Per bloccare eventuali accessi indesiderati utilizziamo un sistema di whitelist tramite le regole di ingress del security group dell’istanza bastione. Ogni accesso SSH è bloccato se non determinati IP che vengono manualmente aggiunti a richiesta.

Schema infrastruttura con bastione

Questo sistema è molto robusto: delega il controllo dell’accesso al security group, una risorsa gestita da AWS, così facendo non dobbiamo farci carico dell’onere di configurazioni del sistema (ad esempio iptables) e gestire varie ed eventuali patch di sicurezza.

IP statici e dinamici

Questa soluzione è ottima quando si parla di IP statici, viene fatta una richiesta al team DevOps di aggiungere in whitelist un determinato IP e questo viene aggiunto tra le regole di ingress per la porta specifica del servizio richiesto (porta 22 per l’accesso SSH). Questo funziona bene quando la richiesta di accesso proviene da altre infrastrutture oppure uffici o coworking che dispongono di un IP fisso.

Essendo la nostra azienda distribuita ed adottando il regime di smart working spesso e volentieri gli sviluppatori oppure il team DevOps si collegano da network con IP dinamici. Questo vuol dire gestire di continuo le regole del security group dell’istanza bastione togliendo ed aggiungendo IP quasi quotidianamente.

Gestione delle regole in modo automatico

Dopo aver vagliato alcune soluzioni abbiamo deciso di adottare un sistema serverless basato su API Gateway e Lambda che in automatico aggiunga l’IP in whitelist sul security group. Per entrare più nel dettaglio: abbiamo esposto un’API HTTP usando il servizio API Gateway di AWS che, ricevuta una richiesta HTTP, innesca l’esecuzione di una Lambda. Quest’ultima recupera l’IP del chiamante dall’evento in input e modifica le regole di ingress del security group per permettergli l’accesso in SSH.

Schema sistema automatico di gestione delle regole

Di per sè il sistema è molto semplice ed essendo basato su servizi serverless, con un traffico molto ridotto, il costo è nullo. Il codice della Lambda è molto semplice:

const AWS = require('aws-sdk');
const ec2 = new AWS.EC2({
  apiVersion: '2016-11-15',
  logger: console
});

/**
 * Configurations
 */
const ROLE_DESCRIPTION = process.env.ROLE_DESCRIPTION || 'managed by lambda';
const SECURITY_GROUP_ID = process.env.SECURITY_GROUP_ID
const API_TOKEN = process.env.API_TOKEN
const PROTOCOL = process.env.PROTOCOL || 'tcp'
const PORT = process.env.PORT || 22

/**
 * Lambda handler
 */
module.exports.handler = async (event) => {
  const sourceIp = event.requestContext.identity.sourceIp;
  
  // Check authorization using token in path parameters
  if(event.pathParameters.token && event.pathParameters.token.toString().trim() === API_TOKEN){
    console.log('Checking authorization for source IP '+sourceIp+' on security group '+SECURITY_GROUP_ID+'..')
    try {

      // Authorize IP on security group
      await ec2.authorizeSecurityGroupIngress({
          GroupId: SECURITY_GROUP_ID,
          IpPermissions: [
            {
                IpProtocol: PROTOCOL,
                FromPort: PORT,
                ToPort: PORT,
                IpRanges: [
                  {
                    CidrIp: sourceIp + '/32',
                    Description: ROLE_DESCRIPTION
                  }
                ]

            }
          ]
      }).promise()
    } catch (err) {
      if (err.code == 'InvalidPermission.Duplicate') {
        // Return invalid request
        return {
          statusCode: 409,
          body: 'IP '+sourceIp+' already allowed for port '+PORT,
          headers: {
            'Content-Type': 'text/html',
          }
        }
      }
      // Return server error
      console.error(err)
      return {
        statusCode: 500,
        body: JSON.stringify(err)
      }
    }

    // Return success response
    return {
      statusCode: 409,
      body: 'IP '+sourceIp+' allowed for port '+PORT,
      headers: {
        'Content-Type': 'text/html',
      }
    }
  }

  // Log unauthorized request
  console.error('not authorized request from '+sourceIp);

  // Return unauthorized response
  return {
    statusCode: 401,
    body: 'Invalid Authorization Token',
    headers: {
      'Content-Type': 'text/html',
    }
  }
}

Come si può notare, per verificare la provenienza della richiesta abbiamo optato per passare un determinato token come un parametro del path. Questo perchè volevamo che l’utilizzo di questo endpoint fosse il più semplice possibile e in questo modo possiamo aprire direttamente un link simile a questo: https://xxxxxxxxx.execute-api.us-east-1.amazonaws.com/stage/whitelist/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy

direttamente nel browser senza dover impostare un header particolare (ad esempio X-API-Key utilizzando il sistema build-in di API Gateway) che avrebbe necessitato, ad esempio, una chiamata curl da terminale.

La configurazione di questo token, ID del security group, protocollo, porta e descrizione viene fatta tramite variabili d’ambiente, in questo modo possiamo eseguire il deploy dello stesso codice della Lambda per ambienti ed infrastrutture diverse.

Pulizia schedulata delle regole

Il security group di default ha un limite di 60 regole (incrementabile fino a 1000 tramite una richiesta ad AWS) che potrebbe venire colpito dopo alcuni mesi di utilizzo del sistema. In aggiunta, questo sistema automatico viene utilizzato esclusivamente da chi si connette utilizzando un IP dinamico, questo vuol dire che l’IP verrà riassegnato a qualcun’altro e deve quindi essere rimosso dalla whitelist.

La soluzione che abbiamo adottato è quella di aggiungere un’altra Lambda, questa volta scatenata da un evento schedultato di CloudWatch, che andrà a ripulire le regole del security group.

Schema automatico di pulizia delle regole del security group

Ovviamente non vogliamo che vengano rimosse anche le regole impostate manualmente per gli IP statici: causerebbe altrimenti un blocco involontario degli accessi di integrazioni di terze parti. Per risolvere questo problema utilizziamo come discriminante la descrizione della regola, impostata tramite la variabile d’ambiente ROLE_DESCRIPTION nella Lambda descritta precedentemente. In questo modo andiamo a ripulire solamente le regole aggiunte tramite questo sistema automatico e concedendo al team DevOps di gestire lo stesso alcune regole manualmente.

Il codice della Lambda anche in questo caso è molto semplice:

const AWS = require('aws-sdk')
const ec2 = new AWS.EC2({
  apiVersion: '2016-11-15',
  logger: console
});

/**
 * Configs
 */
const RULE_DESCRIPTION = process.env.RULE_DESCRIPTION || 'managed by lambda';
const SECURITY_GROUP_ID = process.env.SECURITY_GROUP_ID
const PROTOCOL = process.env.PROTOCOL || 'tcp'
const PORT = process.env.PORT || 22

/**
 * Lambda handler
 */
module.exports.handler = async () => {

  // Build parameters
  const clearedIpPermission = {
      IpProtocol: PROTOCOL,
      FromPort: PORT,
      ToPort: PORT,
      IpRanges: []
  };

  console.log('cleaning all IPs on security group '+SECURITY_GROUP_ID+'..');

  // describe security group
  const { SecurityGroups } = await ec2.describeSecurityGroups({
    GroupIds: [
      SECURITY_GROUP_ID
    ]
  }).promise()

  if(SecurityGroups.length == 0){
    console.log('security group '+SECURITY_GROUP_ID+' not found');
    return
  }
  
  // list all security group rules
  const ipPermissions = SecurityGroups[0].IpPermissions;
  ipPermissions.forEach(function(ipPermission){
    ipPermission.IpRanges.forEach(function(ipRange){
      // Check if rule has the right description
      if(ipRange.Description === RULE_DESCRIPTION){
        // Save rule to delete
        clearedIpPermission.IpRanges.push({
          CidrIp: ipRange.CidrIp
        })
        console.log('going to remove ', ipRange.CidrIp);
      }
    })
  })

  // Check if there are rules to clean
  if(clearedIpPermission.IpRanges.length == 0){
    console.log('no rules to remove');
    return
  }

  console.log('removing '+clearedIpPermission.IpRanges.length+' rules..');

  // Remove security group rules
  try {
    await ec2.revokeSecurityGroupIngress({
      GroupId: SECURITY_GROUP_ID,
      IpPermissions: [
        clearedIpPermission
      ]
    }).promise()
  } catch (err) {
    console.error(err)
    throw err
  }
}

utilizziamo direttamente l’API revokeSecurityGroupIngress passando l’elenco delle regole di ingress con una descrizione specifica.

Conclusione

Questo sistema soddisfa le nostre necessità ed ha un buon compromesso tra sicurezza e usabilità. Questo sistema concede solamente l’accesso da parte di un determinato IP verso il servizio SSH esposto dal bastione, ma non concede l’accesso alla macchina, chi si vuole connettere deve comunque utilizzare la propria chiave SSH personale.

Articolo scritto da