Recientemente, en Buk, nos vimos en la necesidad de migrar el cifrado de datos sensibles desde la gema attr_encrypted al cifrado nativo de Rails (Active Record Encryption) a partir de la versión 7+. Esta migración fue impulsada por la adopción del cifrado nativo en las versiones más recientes de algunas de las gemas que utilizamos, lo que nos llevó a cuestionar la necesidad de mantener dos métodos de cifrado. Además, considerando la evolución de Rails, concluimos que el cifrado nativo es la solución a futuro para nuestras aplicaciones.
Tras analizar ambas opciones, junto con los riesgos asociados, decidimos migrar al cifrado nativo con Active Record Encryption debido a sus diversas ventajas.
Active Record Encryption: Al ser parte del core de Rails, recibe mantenimiento y actualizaciones continuas por el equipo de Rails. Esto asegura que se mantenga al día con las mejores prácticas de seguridad y las nuevas versiones de Rails.
attr_encrypted: Es una gema de terceros. Su mantenimiento depende de la comunidad y de sus mantenedores. Aunque ha sido popular, el soporte y las actualizaciones pueden ser menos consistentes que con una funcionalidad del core.
Generamos las llaves de cifrado:
bin/rails db:encryption:init
Creamos las nuevas variables de entorno con las llaves generadas:
# .env.development
ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY=<valor de primary_key>
ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY=<valor de deterministic_key>
ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT=<valor de key_derivation_salt>
Configuramos el nuevo inicializador de Active Record Encryption:
# config/initializers/active_record_encryption.rb
ActiveRecord::Encryption.configure(
primary_key: ENV['ACTIVE_RECORD_ENCRYPTION_PRIMARY_KEY'],
deterministic_key: ENV['ACTIVE_RECORD_ENCRYPTION_DETERMINISTIC_KEY'],
key_derivation_salt: ENV['ACTIVE_RECORD_ENCRYPTION_KEY_DERIVATION_SALT'],
support_unencrypted_data: true,
store_key_references: true,
encrypt_fixtures: false,
)
Todas las posibles configuraciones de Active Record Encryption pueden ser encontradas en la documentación de Active Record Encryption.
Para no afectar la continuidad operativa, tuvimos que crear nuevos atributos para auxiliar la migración de los datos sensibles de attr_encrypted
a Active Record Encryption
.
Como teniamos el prefijo encrypted_
para los atributos de attr_encrypted
utilizamos los nombres originales de los atributos antes de aplicar attr_encrypted
(sin el prefijo encrypted_
) para los nuevos atributos de Active Record Encryption
.
Por ejemplo, si teniamos un atributo encrypted_token
en attr_encrypted
lo convertimos en token
en Active Record Encryption
.
Para que eso fuera posible, tuvimos que crear métodos wrapper para los atributos cifrados. También hicimos uso de un feature flag para que la migración fuera gradual, permitiendo hacer pruebas controladas internas y posteriormente con algunos clientes y no afectar la continuidad operativa.
Métodos wrapper para consultar el atributo cifrado:
class ApiAuthToken < ApplicationRecord
...
def encryption_key
# retorna la llave de cifrado de attr_encrypted
end
def token
if Buk::Feature.enabled?(:feature_flag) && encrypted_attribute?(:token)
self[:token]
else
iv = Base64.decode64(self[:encrypted_token_iv])
ApiAuthToken.decrypt_token(self[:encrypted_token], iv: iv, key: encryption_key)
end
end
end
Nota:
Buk::Feature.enabled?
es un método para verificar que la feature flag esté activada en Buk, es interno y no nativo de Rails. En caso de que no maneje feature flags, puede ocupar una variable de entorno o quitar el control de la feature flag, ocupando el código interno delelse
.
Antes de agregar los nuevos atributos, tuvimos que controlar el valor del atributo token
para no almacenar el valor sin cifrar. Para eso creamos un método wrapper:
class ApiAuthToken < ApplicationRecord
...
def token=(value)
iv = SecureRandom.random_bytes(12)
encrypted_value = ApiAuthToken.encrypt_token(value, iv: iv, key: encryption_key)
self[:encrypted_token_iv] = Base64.encode64(iv)
self[:encrypted_token] = encrypted_value
self[:token] = nil if ApiAuthToken.column_names.include? "token"
end
...
end
Nota: No podríamos simplesmente usar el
self.ignored_columns
ya que el nombre del atributo es el mismo utilizado enattr_encrypted
.
Después que el código anterior estaba en producción, creamos una nueva migración para agregar los nuevos atributos:
class AddTokenToApiAuthToken < ActiveRecord::Migration[7.0]
def up
add_column :api_auth_tokens, :token, :string
end
def down
remove_column :api_auth_tokens, :token
end
end
Una vez que la migración anterior fue ejecutado en producción, agregamos el cifrado nativo de Rails 7+.
class ApiAuthToken < ApplicationRecord
...
encrypts :token, deterministic: true
...
end
Nota:
deterministic: true
es para que el cifrado sea determinista, es decir, que el mismo valor siempre se cifre de la misma manera. Así podremos consultar el valor del atributotoken
cifrado en la base de datos, sin tener que ocupar su valor original.
Para mantener el valor del atributo token
y el valor cifrado del atributo encrypted_token
sincronizados, actualizamos el método wrapper de asignación de valor (ejemplo: token=
):
class ApiAuthToken < ApplicationRecord
...
def token=(value)
iv = SecureRandom.random_bytes(12)
encrypted_value = ApiAuthToken.encrypt_token(value, iv: iv, key: encryption_key)
self[:encrypted_token_iv] = Base64.encode64(iv)
self[:encrypted_token] = encrypted_value
self[:token] = value
end
...
end
Para atributos que necesitamos consultar, en este ejemplo el token
, hicimos un método wrapper que nos permitía decidir que atributo consultar, token
o encrypted_token
dependiendo de si la feature flag estaba activada o no:
class ApiAuthToken < ApplicationRecord
...
# Wrapper para evitar el cache cuando activamos y desactivamos la FF
def self.find_by(conditions)
where(conditions).take
end
# Wrapper para determinar que atributo debemos consultar para el token
def self.where(conditions)
if Buk::Feature.enabled?(:feature_flag)
conditions = conditions.symbolize_keys
if conditions[:encrypted_token].present?
conditions[:token] = conditions[:encrypted_token]
conditions.delete(:encrypted_token)
end
end
super
end
...
end
Nota:
Buk::Feature.enabled?
es un método para verificar que la feature flag esté activada en Buk, es interno y no nativo de Rails. Caso no tenga feature flags en su sistema, puede ocupar una variable de entorno o quitar ese código.
Para sincronizar los datos, hicimos una migración para actualizar los valores de token
con el valor de encrypted_token
.
class SyncApiAuthTokenToken < ActiveRecord::Migration[7.0]
class ApiAuthToken < ApplicationRecord
encrypts :token, deterministic: true
attr_encryptor :encrypted_token, key: :encryption_key, encode: true
def encryption_key
# retorna la llave de cifrado de attr_encrypted
end
def decrypt_token_with_attr_encrypted
iv = Base64.decode64(self[:encrypted_token_iv])
ApiAuthToken.decrypt_token(self[:encrypted_token], iv: iv, key: encryption_key)
end
def token=(value)
iv = SecureRandom.random_bytes(12)
encrypted_value = ApiAuthToken.encrypt_token(value, iv: iv, key: encryption_key)
self[:encrypted_token_iv] = Base64.encode64(iv)
self[:encrypted_token] = encrypted_value
self[:token] = value
end
end
def change
ApiAuthToken.all.each do |token|
token.token = token.decrypt_token_with_attr_encrypted
token.save!
end
end
end
Si utiliza una Feature Flag o variable de entorno para controlar qué variable cifrada debe devolver el valor, puede habilitarla gradualmente en sus entornos y observar el comportamiento del sistema. Una vez que alcance el 100% del despliegue, puede continuar con el paso 6.
De lo contrario, puede omitir este paso (ir al paso 6).
Creamos una migración para eliminar el not null del atributo encrypted_token
y encrypted_token_iv
:
class RemoveNotNullFromEncryptedToken < ActiveRecord::Migration[7.0]
def up
change_column_null :api_auth_tokens, :encrypted_token, true
change_column_null :api_auth_tokens, :encrypted_token_iv, true
end
def down
change_column_null :api_auth_tokens, :encrypted_token, false
change_column_null :api_auth_tokens, :encrypted_token_iv, false
end
end
Debemos eliminar todo el código relacionado con la gema attr_encrypted, incluyendo los helpers y los métodos wrapper que hicimos para consultar el atributo cifrado. Como utilizamos el nombre original del campo, todo quedará limpio.
Todo el código de attr_encrypted debe ser eliminado. Ejemplo:
attr_encryptor :encrypted_token, key: :encryption_key, encode: true
Los métodos encryption_key
, token=
, token
, self.find_by
y self.where
también deben ser eliminados.
Solo debería quedar ese código en el modelo:
encrypts :token, deterministic: true
Que es el código que usamos para cifrar el atributo token
con el cifrado nativo de Rails 7+.
Debemos eliminar los atributos de la gema attr_encrypted (en el caso de nuestro ejemplo encrypted_token
y encrypted_token_iv
) con una migración.
class RemoveEncryptedTokenFromApiAuthToken < ActiveRecord::Migration[7.0]
def up
remove_column :api_auth_tokens, :encrypted_token
remove_column :api_auth_tokens, :encrypted_token_iv
end
def down
add_column :api_auth_tokens, :encrypted_token, :string
add_column :api_auth_tokens, :encrypted_token_iv, :string
end
end
La integridad de los datos, un aspecto crítico en el desarrollo de software, debe ser garantizada durante esta migración. Por lo tanto, es esencial que documentemos minuciosamente los cambios realizados y el nuevo método de cifrado de atributos. Esto nos permitirá entender el proceso de migración y hacerlo de manera correcta, principalmente para prevenir errores en el futuro.
Active Record Encryption se presenta como una alternativa de cifrado moderna, segura y con un mantenimiento simplificado para aplicaciones Rails. Al estar integrado en el núcleo de Rails, ofrece una experiencia de desarrollo superior, un rendimiento potencialmente optimizado y una seguridad reforzada a largo plazo, consolidándose como la opción preferente para nuevos proyectos y actualizaciones.
El proceso de migración desde attr_encrypted
hacia Active Record Encryption
requiere una cuidadosa planificación y ejecución. Antes de realizar cualquier cambio, es crucial identificar todos los modelos que utilizan attr_encrypted
, así como evaluar los riesgos y posibles fallas inherentes al proceso de migración. Con toda la información recopilada, se podrá proceder a la planificación detallada y, posteriormente, a la ejecución de la migración.
La implementación de pruebas automatizadas es fundamental a lo largo de todo el proceso de migración, con el fin de validar el correcto funcionamiento en cada etapa y mitigar el riesgo de incidencias en producción.
La prioridad máxima debe ser la continuidad operativa y la seguridad de los datos. Para ello, se recomienda trabajar con atributos nuevos y, si es posible, emplear scripts de validación que aseguren la sincronización de los datos en el entorno de producción, minimizando así el riesgo de sorpresas inesperadas.
Se ha elaborado una guía detallada del proceso de migración, tomando como ejemplo práctico el modelo ApiAuthToken
. Es importante que este procedimiento se aplique de manera similar a todos los modelos reales del proyecto que dependen de la gema attr_encrypted
para el cifrado de datos.