Как стать автором
Обновить

Reactive Spring ABAC Security: безопасность уровня Enterprise

Время на прочтение23 мин
Количество просмотров7.8K

В продолжение предыдущей статьи Передовые технологии на службе СЭД рассмотрим современные подходы к обеспечению корпоративной безопасности и ожидаемые системные риски.

Аннотация

Что такое безопасность уровня Enterprise? Встречается огромное разнообразие схем конфигурации безопасности инфраструктуры. Например: валидация и кэширование токена только в сервисе Gateway с прямой отправкой логина и списка ролей сервисам или ретрансляция токена с Gateway в сервисы для активации функций Spring Security через распаковку токена в логин и роли с проверкой только подписи токена и т.д. и т.п.

На самом деле, любую существующую схему безопасности можно отнести к Enterprise. Базовая конфигурация Spring Security, предоставляемая по умолчанию, не выдерживает ни высоких ни средних нагрузок. Данный фактор стал одним из главных причин разнообразия подходов к безопасности, возникших в поиске оптимального способа поддержки высоких нагрузок в рамках существующей инфраструктуры и технологий.

В статье рассматриваются основные аспекты конфигурирования корпоративной безопасности с поддержкой высоких нагрузок в реактивном стеке технологий. Подробно описана модель безопасности attribute-based access control (ABAC), которая применяет совокупность абстрактных правил к определённому типу действий, с учётом роли при необходимости.

Перечисляются видимые риски более высокого уровня с обзором их снижения или обхода. А также, некоторый поучительный исторический экскурс с акцентом на до сих пор возможные риски безопасности.

Введение

Начнём с рассмотрения основополагающей на данный момент модели безопасности ABAC в сравнении с классической моделью role-based access control (RBAC).

Классическая модель безопасности RBAC предлагает грубый подход, не позволяющий описать политику безопасности многоуровневой филиальной системы.

RBAC оперирует ролями и группами ролей. Каждая роль напрямую привязана к действию, что приводит к созданию большого количества ролей (роль менеджера головной организации, роль менеджера филиала, роль менеджера подразделения филиала – разные сущности в силу функциональных различий).

Большое множество ролей не позволяют эффективно выстроить политики безопасности, группирующие типичные действия из-за неочевидности их разграничения. Модель RBAC стала архаичной не смотря на сегодняшнее присутствие в большинстве информационных систем.

Основное преимущество модели безопасности ABAC в возможности описания политики безопасности через совокупность правил, основанных на атрибутах участвующих в действиях пользователя. Такой подход позволяет детализировать политики безопасности до необходимого уровня.

Целевое направление применения ABAC – описание политик безопасности филиальной системы с полным контролем доступа до любого уровня вложенности и сложности описания правил. Все правила ABAC описываются в виде SpEL-выражений и в отличии от RBAC хранятся в базе данных, где соответственно легко поддаются анализу и модификации.

Можно отметить интересный факт связанный с переходом информационной безопасности от модели RBAC к ABAC – на первом этапе каждая политика будет содержать одно правило.

Предлагаемые в статье технологии и подходы в полной мере реализуют концепцию ABAC с поддержкой высоких нагрузок за счёт применения каскадного кэширования с гарантией консистентности.

В итоге, реализована библиотека Reactive Spring ABAC Security с открытым исходным кодом на GitHub, обладающая рядом свойств: как гибкость в настройках, мощь в широких возможностях и скорость в работе. Библиотека опубликована в Maven Central:

<dependency>
    <groupId>io.github.sevenparadigms</groupId>
    <artifactId>reactive-spring-abac-security</artifactId>
    <version>1.0.4</version>
</dependency>

Исходного кода получилось немало, поэтому, выделим в статье только часть касательно ABAC и особенно важных моментов. Примечательно, что реализация ABAC крайне проста при столь гибком функционале.

Через настройки библиотеки можно выбрать любую гомоморфную исходной схему конфигурации безопасности из коробки, а также генерировать или валидировать токен – в совокупности, такая гибкость и излишняя функциональность становится особенно удобной при разработке прототипа проекта на начальной стадии с целью точной оценки итоговых сроков реализации или участия в тендере, продемонстрировав готовый прототип с основным функционалом, в то время, как у конкурентов ничего нет.

Совсем необязательно поднимать дополнительный корпоративный SSO-сервер аутентификации типа Keycloak для задач такого рода, когда скорость реализации и простота развёртывания выходят на передний план – для таких задач достаточно фрилансера, без отрыва внутренних ресурсов от основных задач.

Предлагаемая библиотека на клиентской стороне активирует Spring Security с моделью безопасности ABAC, при этом, кэшируя данные по токену, обратившись к сервису авторизации только раз. Пометка отозванности токена в кэше происходит через событие Spring Event, инициацию которого оставляем за логикой инфраструктуры при завершении сессии пользователя.

Основные аспекты реализации

Рассмотрим основные моменты при настройке конфигурации безопасности Spring Security:

@Bean
fun securityWebFilterChain(
    http: ServerHttpSecurity,
    authenticationWebFilter: AuthenticationWebFilter,
    abacRulePermissionService: AbacRulePermissionService,
    expressionHandler: DefaultMethodSecurityExpressionHandler
): SecurityWebFilterChain {
    expressionHandler.setPermissionEvaluator(abacRulePermissionService)
    http.csrf().disable()
        .headers().frameOptions().disable()
        .cache().disable()
        .and()
        .exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint())
        .and()
        .authorizeExchange()
        .pathMatchers(HttpMethod.OPTIONS)
        .permitAll()
        .and()
        .requestCache().requestCache(NoOpServerRequestCache.getInstance())
        .and()
        .authorizeExchange()
        .matchers(EndpointRequest.toAnyEndpoint())
        .hasAuthority(Constants.ROLE_ADMIN)
        .and()
        .authorizeExchange()
        .pathMatchers(*Constants.whitelist).permitAll()
        .anyExchange().authenticated()
        .and()
        .securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
        .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
        .httpBasic().disable()
        .formLogin().disable()
        .logout().disable()
    return super.tryAddTokenIntrospector(http).build()
}

Строка expressionHandler.setPermissionEvaluator(abacRulePermissionService) включает функционал ABAC. Все правила ABAC представляют собой компилируемые и кэшируемые SpEL-выражения, предоставляя полную свободу в предикатах. Подробное описание подключения ABAC в разделе настройки безопасности на стороне клиентского сервиса через файл конфигурации application.yml

По умолчанию, при первом запросе клиента, Spring создаёт web-сессию для кэширования сессионных переменных и результата запросов, а также сбора различных данных при взаимодействии с клиентом в рамках сессии. Данный подход устарел – сессии занимают значительный объем памяти и вся логика вокруг сессий не имеет перспектив, не говоря уже о проблемах утечки в реактивном стеке.

На много проще и увереннее поддерживать жизнь сессии в рамках одной реактивной цепочки инструкций запрос-ответа, после чего, сессия благополучно закроется вместе с сессионным потоком. Кто-то возразит и скажет, что кэширование сессий экономит до тысячи инструкций на её создание, но тут дело в совокупности факторов: неоднозначность данной устаревшей оптимизации пришедшей с наследством из синхронного стека, высокой эффективности современных процессоров, когда тысяча инструкций выполняются менее чем за 1 мс и риске утечки памяти реактивного стека.

Следующей строкой отключаем поддержку сессий: securityContextRepository(NoOpServerSecurityContextRepository.getInstance())

В Webflux была проблема #7157 связанная с автоматическим кэшированием в сессиях, поэтому также отключаем сессионное кэширование строкой: requestCache().requestCache(NoOpServerRequestCache.getInstance()), чтобы Webflux оперировал только кэшем уровня бизнес-логики и только в контексте потока исполняемого запроса.

По умолчанию, Spring Security не кэширует токен из-за постоянной необходимости проверки токена на отозванность в сервисе авторизации. Но допустим, если взять с десяток сервисов с разной степенью нагруженностью в зависимости от решаемых бизнес-задач, разнесённых в десятки подов, то сервису авторизации понадобится больше сотни подов из-за чрезмерной нагрузки, т.к. один внешний запрос может каскадно индуцировать подзапросы на все сервисы и создавая таким образом подобие DDoS-атаки на сервис авторизации.

Возможность кэширования токенов предусмотрена в библиотеке Spring OAuth2 через расширение OpaqueToken и кэширующий интроспектор NimbusOpaqueToken, который единожды валидирует токен в сервисе авторизации. Заменим интроспектор на свою реализацию, т.к. в нашей библиотеке не используются автоконфиги Spring OAuth2, не смотря на заимствование функционала, и включим в конфигурацию строкой: http.oauth2ResourceServer().opaqueToken().introspector(super.tokenIntrospector())

Появилась прямая необходимость в регистрации бина билдера WebClient сразу по нескольким причинам:

@Bean
fun webClientBuilder(): WebClient.Builder = WebClient.builder()
    .clientConnector(
        ReactorClientHttpConnector(
            HttpClient.create(
                ConnectionProvider.builder("fixed")
                    .maxConnections(500)
                    .maxIdleTime(Duration.ofSeconds(30))
                    .maxLifeTime(Duration.ofSeconds(60))
                    .pendingAcquireTimeout(Duration.ofSeconds(60))
                    .evictInBackground(Duration.ofSeconds(120)).build()
            ).keepAlive(false)
        )
    )
    .exchangeStrategies(ExchangeStrategies.builder()
        .codecs { c: ClientCodecConfigurer ->
            c.customCodecs().register(Jackson2JsonDecoder(JsonUtils.getMapper()))
            c.customCodecs().register(Jackson2JsonEncoder(JsonUtils.getMapper()))
        }
        .build())
    .filter { clientRequest: ClientRequest, next: ExchangeFunction ->
        Mono.deferContextual { ctx: ContextView ->
            val requestHeaders = ctx.get(ServerWebExchange::class.java).request.headers.toSingleValueMap()
            val request =
                ClientRequest.from(clientRequest).headers { headers -> headers.setAll(requestHeaders) }.build()
            next.exchange(request)
        }
    }
    .codecs { it.defaultCodecs().apply { maxInMemorySize(16 * 1024 * 1024) } }

чтобы поправить маленький таймаут соединения, решить проблему ограничения пула в 16 соединений, зарегистрировать настроенный ObjectMapper, включить ретрансляцию заголовков и увеличить размер буфера обмена – работа с WebClient с дефолтными настройками со временем может привести к ряду ошибок, например 'Connection reset by peer'.

Также решилась проблема Netty с постоянным ростом незакрытых соединений при высокой нагрузке:

@Bean
fun nettyWebServerFactoryCustomizer() = WebServerFactoryCustomizer<NettyReactiveWebServerFactory> {
    it.addServerCustomizers(
        NettyServerCustomizer { server: HttpServer ->
            server.doOnConnection { connection: Connection ->
                connection.addHandler(
                    object : IdleStateHandler(0, 0, 0) {
                        override fun channelIdle(ctx: ChannelHandlerContext, evt: IdleStateEvent) {
                            ctx.fireExceptionCaught(
                                if (evt.state() == IdleStateEvent.WRITER_IDLE_STATE_EVENT.state())
                                    WriteTimeoutException.INSTANCE
                                else
                                    ReadTimeoutException.INSTANCE
                            )
                            ctx.write(CloseWebSocketFrame())
                            ctx.close()
                        }
                    }
                )
            }
        }
    )
}

Для удобства доступа к контексту безопасности помимо реактивного ExchangeHolder, был добавлен синхронный ExchangeContext, т.к. часто контекст безопасности запрашивается в статических методах вне реактивной цепочки:

object ExchangeHolder {
    fun getHeaders(): Mono<MultiValueMap<String, String>> {
        return Mono.deferContextual { ctx: ContextView ->
            Mono.just(
                ctx.get(ServerWebExchange::class.java).request.headers as MultiValueMap<String, String>
            )
        }
    }

    fun getResponse(): Mono<ServerHttpResponse> {
        return Mono.deferContextual { ctx: ContextView ->
            Mono.just(ctx.get(ServerWebExchange::class.java).attributes[RESPONSE] as ServerHttpResponse)
        }
    }

    fun getUser(): Mono<User> {
        return Mono.deferContextual { ctx: ContextView ->
            ctx.get(ServerWebExchange::class.java).getPrincipal<Principal>()
                .cast(UsernamePasswordAuthenticationToken::class.java)
                .map { it.principal }
                .cast(User::class.java)
        }
    }
}

Также для удобства библиотека отключает CORS и CSRF в связи с настройкой данной защиты на уровне HaProxy или Gateway перед сервисом:

 override fun addCorsMappings(corsRegistry: CorsRegistry) {
        corsRegistry.addMapping("/**")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedOriginPatterns("*")
            .allowedHeaders("*")
            .maxAge(3600)
    }

В библиотеку включено активно разрабатываемый проект jjwt, обладающий наибольшими возможностями при работе с JWT-токенами. Также используется проект r2dbc-dsl для инициализации репозитория через url к базе данных:

@Bean
fun abacRuleRepository(@Value("\${spring.security.abac.url}") url: String) =
    R2dbcUtils.getRepository(url, AbacRuleRepository::class.java)

Предлагаемое R2DBC Spring Data конфигурирование соединений к нескольким базам данных чересчур громоздко для инициализации всего одного репозитория. В ближайшем будущем будет произведена интеграция r2dbc-dsl со Spring Security на уровне контекста безопасности с привязкой на заднем фоне всех SQL-запросов к идентификаторам userId и tenantId для автоматической фиксации в базе данных вне бизнес-логики.

Процесс бизнес-логирования действий пользователя порой занимает до 50% от всей нагрузки на базу данных и решение этой задачи критично для любого проекта. Обычно она решается через создание виртуального потокового репликатора PostgreSQL с сохранением собранных данных через команду Copy или можно воспользоваться библиотекой PgBulkInsertPublic – такой подход к бизнес-логированию полностью разгружает БД.

Настройка безопасности клиента

Рассмотрим файл конфигурации application.yml из демонстрационного проекта webflux-dsl-abac-example:

spring:
  r2dbc:
    url: r2dbc:postgresql://postgres:postgres@localhost/dsl_abac?schema=public
    pool:
      maxSize: 20
  main:
    allow-bean-definition-overriding: true
  security:
    abac.url: r2dbc:postgresql://postgres:postgres@localhost/abac_rules?schema=public
    iteration: 512
    length: 720
    secret: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ1c2VyIiwiYXV0aCI6IlJPTEVfVVNFUiIsImV4cCI6MTYzMDYxMDI5NX0.m0XU2NvGaAtzptgLfmptj3Fk7S1e1NrBTYTqBAjHoPI8lbRB7z3J52FiLRw-PUZPjQusDt19RszrUQDsZoVXeQ
    expiration: 1800
    X-User-Id: true
    skip-token-validation: true
    cache-token: true

Как видим, есть основное соединение с базой данных, а есть отдельная база данных abac_rules, в которой аккумулируются правила со всех сервисов, что удобно в управлении безопасностью и появляется возможность переиспользования правил.

С точки зрения атаки, когда вся безопасность сервисов вынесена в БД – возникает новая цель в виде таблицы с правилами. Сервисы могут читать свои правила из общей таблицы за счёт применения безопасности со стороны PostgreSQL защиты на уровне строк, когда пользователь БД может видеть только свои записи. Если пароли сервиса скомпрометированы, то атакующий может увидеть правила безопасности сервиса и запланировать стратегию атаки для получения конфиденциальной информации.

При описании правил ABAC лучше всего учитывать принципы наименьших привилегий и тогда маловероятно, что будет достигнут хоть какой-либо успех атакующим, но попытки необычных запросов легко зафиксировать и отразить в журнале логов для оповещения уполномоченной группы.

Необязательные переменные iteration и length используются для генерации секретного ключа и соли системного пароля пользователя. Необязательная переменная secret используется как часть секрета для генерации пароля пользователя и токена. Переменная expiration имеет значение времени жизни токена в секундах.

Переменные iteration, length, secret являются критическими для безопасности и в общем, рекомендуется хранить все значения переменных окружения на сервере секретов типа HashiCorp Vault.

X-User-Id при значении true активирует Spring Security, когда ожидается в заголовке X-User-Id значение id пользователя, а в X-Roles массив ролей. Имя пользователя в Spring Security указывается как пустая строка, если отсутствует заголовок X-Login.

Такая беззащитная конфигурация значительно ускоряет работу с ролевой моделью, но явно открывает сервис атакующему из локальной сети. Для защиты такой конфигурации обычно включают межсервисную авторизацию между sidecar в Istio и строгую сетевую политику ограничения межсервисных доступов.

Необязательная переменная skip-token-validation при значении true активирует Spring Security, извлекая из токена имя пользователя и роли, где присутствует единственная проверка на время жизни токена. Такая же беззащитная конфигурация как и предыдущая, но позволяет передать сервису работу по распаковке токена вместо Gateway. Защищается с помощью Istio.

Необязательная переменная cache-token при значении true активирует Spring Security с полной валидацией токена и кэшированием данных токена. В случае, если в конфигурации не прописан путь до сервиса авторизации spring.security.introspection.uri, тогда ожидается, что токен каким-то образом уже лежит в текущем CacheManager – это удобно при кустомном способе авторизации пользователя, а иначе CacheManager используется для кластерного кэширования. Вместо пути до сервиса авторизации можно указать в переменной spring.security.public сохраненный в Base64 публичный ключ токена для проверки подписи.

Это основная конфигурация безопасности с валидацией токена, кэшированием и возможностью отзыва, не требует Istio. За счёт кэширования получаем максимально возможную пропускную способность от реактивного стека. При использовании публичного ключа заметно увеличивается нагрузка на процессор. Все представленные конфигурации можно комбинировать.

Практическое применение ABAC

Скрипт создания таблицы с правилами:

CREATE TABLE abac_rule
(
    id             uuid DEFAULT uuid_generate_v1mc() NOT NULL PRIMARY KEY,
    name           text,
    domain_type    text,
    target         text,
    condition      text
);

insert into abac_rule(name, domain_type, target, condition)
values('Test Rule', 'Dsl', 'action == ''findAll'' and subject.roles.contains(''ROLE_ADMIN'')', 'domainObject.sort == ''id:desc'''),
      ('IP Rule', 'Dsl', 'action == ''findAll'' and environment.ip == ''192.168.2.207''', 'domainObject.sort == ''id:desc'''),
      ('Query jtree not null Rule', 'Dsl', 'action == ''findAll'' and subject.roles.contains(''ROLE_ADMIN'')', 'domainObject.query == ''!@jtree'' and domainObject.fields ==''id''  and domainObject.sort == ''id:desc'''),
      ('Query equals jsonb field Rule', 'Dsl', 'action == ''findAll'' and subject.roles.contains(''ROLE_ADMIN'')', 'domainObject.query == ''jtree.name==Acme doc'' and domainObject.sort == ''id:desc'''),
      ('Query equals jsonb field in Rule', 'Dsl', 'action == ''findAll'' and subject.roles.contains(''ROLE_ADMIN'')', 'domainObject.query == ''jtree.name^^Acme doc'' and domainObject.sort == ''id:desc''');

Применение правила в аннотации над методом:

@PreAuthorize("hasPermission(#dsl, 'findAll')")
fun findAll(@PathVariable jfolderId: UUID, dsl: Dsl) 
                                         = objectService.findAll(jfolderId, dsl)

Как уже ранее указывалось, выражения ABAC в виде SpEL компилируется и кэшируются. Если взглянуть на код, то логика очень простая:

@Component
class ExpressionParserCache : ExpressionParser {
    private val cache: MutableMap<String, Expression> = ConcurrentHashMap(720)
    private val parser: ExpressionParser = SpelExpressionParser()

    @Throws(ParseException::class)
    override fun parseExpression(expressionString: String): Expression {
        return cache.computeIfAbsent(expressionString) {
            parser.parseExpression(it)
        }
    }

    @Throws(ParseException::class)
    override fun parseExpression(expressionString: String, 
                                 context: ParserContext): Expression {
        throw UnsupportedOperationException("not supported")
    }
}

Затем перегружается стандартный метод безопасности hasPermission:

@Service
class AbacRulePermissionService(
    private val abacRuleRepository: AbacRuleRepository,
    private val expressionParserCache: ExpressionParserCache,
    private val exchangeContext: ExchangeContext
) : DenyAllPermissionEvaluator() {
    override fun hasPermission(authentication: Authentication,
                               domainObject: Any, action: Any): Boolean {
        debug("Secure user %s action '%s' on object %s", authentication.name, action, domainObject)
        val user = authentication.principal as User
        return checkIn(
            AbacSubject(user.username, user.authorities.map { it.authority }.toSet()),
            domainObject,
            action as String
        )
    }

    private fun checkIn(subject: AbacSubject, domainObject: Any, action: String): Boolean {
        var result = false
        runBlocking {
            val context = AbacControlContext(
                subject, domainObject, action, 
                AbacEnvironment(ip = exchangeContext.getRemoteIp(subject.username))
            )
            result = abacRuleRepository.findAllByDomainType(domainObject.javaClass.simpleName)
                .filter { abacRule: AbacRule ->
                    expressionParserCache.parseExpression(abacRule.target).getValue(
                        context,
                        Boolean::class.java
                    )!!
                }
                .any { abacRule: AbacRule ->
                    expressionParserCache.parseExpression(abacRule.condition)
                        .getValue(context, Boolean::class.java)!!
                }
                .awaitFirst()
        }
        return result
    }
}

Сначала формируется контекст в виде модели AbacControlContext, содержащий поля: subject с логином и ролями пользователя, domainObject – входящий объект, action – наименование правила и AbacEnvironment с текущей датой и ip клиента.

Как видно из кода, при поиске всех правил по атрибуту domain (имя класса из domainObject), сначала фильтруем по SpEL выражению из атрибута target и если найдены правила, то по ним в атрибуте condition прогоняем итоговое SpEL выражение.

Для включения в проекте Spring Security вместе с моделью безопасности ABAC достаточно добавить аннотацию @EnableAbacSecurity в класс Application.

Более подробно применение ABAC можно посмотреть в исходном коде тестов демонстрационного проекта webflux-dsl-abac-example.

В тестовом окружении демо-проекта в качестве используемой базы данных поднимается контейнер postgres-rum – что невероятно удобно и практично:

private fun createContainer(): KContainer =
    KContainer(DockerImageName.parse("jordemort/postgres-rum:latest")
        .asCompatibleSubstituteFor("postgres"))
        .withDatabaseName("test-db")
        .withUsername("postgres")
        .withPassword("postgres")
        .withCreateContainerCmdModifier { cmd ->
            cmd.withHostConfig(
                HostConfig().withPortBindings(
                    PortBinding(
                        Ports.Binding.bindPort(5432), ExposedPort(5432)
                    )
                )
            )
        }
        .withReuse(true)
        .withCopyFileToContainer(
            MountableFile.forClasspathResource("init.sql"),
            "/docker-entrypoint-initdb.d/"
        )

Пример теста из демо-проекта, проверяющий потоковую изолированность извлечения контекста безопасности из реактивной цепочки:

@Test
fun `test Holder race condition by two users`() {
    val flux = Flux.range(1, 100)
        .parallel(10)
        .runOn(Schedulers.boundedElastic())
        .flatMap { rangeCount ->
            val testIp: String
            if (rangeCount % 2 == 0) {
                testIp = nonCorrectIp + rangeCount
                webClient.get()
                    .uri("dsl-abac/context")
                    .header(HttpHeaders.AUTHORIZATION, Constants.BEARER + adminToken)
                    .header(Constants.AUTHORIZE_IP, testIp)
                    .exchangeToMono { it.bodyToMono(List::class.java) }
                    .zipWith(Mono.just(testIp))
            } else {
                testIp = correctIp + rangeCount
                webClient.get()
                    .uri("dsl-abac/context")
                    .header(HttpHeaders.AUTHORIZATION, Constants.BEARER + userToken)
                    .header(Constants.AUTHORIZE_IP, testIp)
                    .exchangeToMono { it.bodyToMono(List::class.java) }
                    .zipWith(Mono.just(testIp))
            }
        }

    StepVerifier.create(flux)
        .expectNextCount(98)
        .expectNextMatches { it.t2 == it.t1[1] }
        .expectNextMatches { it.t2 == it.t1[1] }
        .thenCancel()
        .verify()
}

Еще один интересный пример использования данной библиотеки – проект реактивной Spring Cloud Gateway, который публикует реактивный вебсокет для конвертации запросов из вебсокета в http-запросы к сервисам и обратно. Чтобы подключиться к вебсокету необходимо передать jwt-токен, который валидируется только по времени, публичному ключу, а статус отозванности обновляется через событие kafka, т.е. максимальное ускорение.

Логика запуска реактивного вебсокета с ожиданием авторизации в Spring Security занимает всего несколько строк:

@Component
@WebsocketEntryPoint("/wsf")
class WebsocketFactory : WebSocketHandler {
    override fun handle(session: WebSocketSession): Mono<Void> {
        return session.handshakeInfo.principal
            .cast(UsernamePasswordAuthenticationToken::class.java)
            .flatMap { authenticationToken: UsernamePasswordAuthenticationToken ->
                val input = session.receive().map { obj: WebSocketMessage -> obj.payloadAsText }
                    .map { it.parseJson(MessageWrapper::class.java) }
                    .doOnNext { handling(it, authenticationToken) }.then()
                val output =
                    session.send(Flux.create {
                        clients[authenticationToken.name] = WebsocketSessionChain(session, it)
                    })
                Mono.zip(input, output).then().doFinally { signal: SignalType ->
                    clients.remove(authenticationToken.name)
                    info("WebSocket revoke connection with signal[${signal.name}] and user[${authenticationToken.name}]")
                }
            }
    }

Фронт отправляет и принимает запросы в обвёртке:

data class MessageWrapper(
    val type: HttpMethod = HttpMethod.GET,
    val baseUrl: String = StringUtils.EMPTY,
    val uri: String = StringUtils.EMPTY,
    val body: JsonNode = JsonUtils.objectNode()
)

при ответе body заменяется ответом с сервиса как JSON: message.copy(body = it), а uri служит идентификатором запроса в обработчике вебсокета на фронте.

Функция преобразования запросов вебсокета в RESTful и обратно:

fun handling(message: MessageWrapper, authenticationToken: UsernamePasswordAuthenticationToken) {
    clients[authenticationToken.name].access = LocalDateTime.now()
    val webClient = Beans.of(WebClient.Builder::class.java).baseUrl(message.baseUrl).build()
    when (message.type) {
        HttpMethod.GET -> webClient.get().uri(message.uri).retrieve()
        HttpMethod.POST -> webClient.post().uri(message.uri).body(BodyInserters.fromValue(message.body)).retrieve()
        HttpMethod.PUT -> webClient.put().uri(message.uri).body(BodyInserters.fromValue(message.body)).retrieve()
        HttpMethod.DELETE -> webClient.delete().uri(message.uri).retrieve()
    }.bodyToMono(JsonNode::class.java).subscribe {
        info("Request[${message.baseUrl}${message.uri}] by user[${authenticationToken.name}] accepted")
        clients[authenticationToken.name].sendMessage(message.copy(body = it))
    }
}

Нагрузочное тестирование

Тестирование проводилась на обычной рабочей станции со средними характеристиками и с дефолтными настройками r2dbc (без увеличенных таймаутов и пула соединений). При тестировании демо-проект с библиотекой ABAC стабильно выдерживает 100 одновременных запросов контекста безопасности через REST каждые 20 мс.

При уменьшении периода вызова и увеличении одновременных запросов возникали ошибки в R2DBC Spring Data – обычного размера пула в 20 соединений не хватило, т.к. некоторые запросы падали по таймауту из-за нехватки доступных соединений. Главная причина такого поведения – полное отсутствие кэша 1-го уровня в реактивном Spring Data.

Срочным образом, в библиотеку r2dbc-dsl был добавлен кэш 1-го уровня, причём его можно использовать и как кэш 2-го уровня без in-memory базы данных, т.к. достаточно подписаться на изменения нужных таблиц через application.yml

Кэш 1-го уровня – это технический кэш, используемый в рамках одного действия, после чего, кэш должен очистится. В то время, как кэш 2-го уровня живёт значительно дольше и обеспечивает именно бизнес-логику. По умолчанию, кэш 1-го уровня в библиотеке r2dbc-dsl живёт 500 мс, что достаточно для логики одного действия и быстрее, чем пользователь успеет выполнить следующее действие. Кэш 1-го уровня часто используется как кэш глобального уровня, что значительно упрощает и сокращает исходный код.

Кэш 1-го уровня повышает стабильность и производительность, но только в случае правильной реализации. К примеру, проблемой кэша 1-го уровня в Hibernate является её вечная жизнь и невозможность принудительного обновления или отключения, что приводит к ограничениям и логическим ошибкам бизнес-логики. Hibernate по умолчанию хранит в кэше 1-го уровня свыше миллиона записей, но даже исправив размер кэша в одну запись – проблем это не решает.

После подключения кэша 1-го уровня, выдерживаемая нагрузка составила более 10 тыс. запросов в секунду, что быстрее синхронной Spring Security примерно в 15 раз. Производительность реактивного стека явно ограничивается процессорным временем и шириной канала связи.

Наиболее прогрессивной in-memory базой данных на мой взгляд является Hazelcast из-за поддержки старта как Embedded вместе с сервисом и поддержки кластера Kubernetes между подами одного сервиса. К сожалению, последняя версия насквозь пронизана уязвимостями с количеством более 10 штук. Поэтому, создал безопасную обвёртку вокруг Hazelcast, работающая в Kubernetes как межподовый кластер сервиса без внешнего доступа. Данная библиотека активирует в Spring Security кэш 2-го уровня с единым хранилищем для всех подов одного сервиса.

Системные риски безопасности

Терминология вокруг системного риска сводится к превентивному взлому, от которого практически невозможно защитится. При этом, охватывается масштаб страны целиком. Вся политика безопасности сервисов должна учитывать возможность атаки с локального хоста и компрометации всех паролей.

Microsoft, согласно статьям тех лет, и на самом деле, точно не известно правда это или вымысел – до 2013 года встраивала в свои ОС дыры и сообщала о выявленных спецслужбам США. Диванные эксперты считают, что в настоящее время взаимодействие всех корпораций со спецслужбами происходит на коммерческой основе для формирования баланса между репутационным имиджем и уровнем технологичности ведения разведки.

В новостях 2013-2014 годов иногда проскакивало, что были взломаны правительственные сервера Китая используя маршрутизаторы Cisco. Последователи теории заговора предполагают, что маршрутизаторы подменили во время таможенного досмотра на территории США, но всё это лишь слухи.

Возможно, в результате данного инцидента и скорее всего с текущего 2022 года – правительственные учреждения, подрядчиковые организации и финансовые институты Китая начнут работать с маршрутизаторами, серверами и операционными системами только отечественного производства.

Есть и оборотная сторона, согласно материалам Bloomberg, от которых вскоре отказались – с 2013 по 2015 года в Китае было произведено десятки тысяч серверов Supermicro с дополнительным чипом 2х3 мм, которые в массовом порядке поставлены сотням компаний по всей планете. Из них 7000 серверов были установлены в Apple. Также сервера доставили подрядчику Министерства обороны США Elemental, который устанавливал их в бортовые сети боевых кораблей ВМФ, центры управления беспилотников ЦРУ, центры обработки данных Министерства обороны, NASA, обе палаты Конгресса, подразделение внутренней безопасности Госдепа, а также в Amazon.

Легенды с просторов Интернета гласят, что в результате этой атаки, все представляющие интерес сервера Supermicro с выходом в Интернет были взломаны и на десятки анонимных серверов выгружены гигабайты информации. Микрокод встроенного чипа регулярно с разным периодом времени пытался установить соединение с рядом действующих DNS-серверов в Интернете, имитируя dns-запрос и как только соединение устанавливалось, сразу загружалась подпрограмма сканирования и идентификации.

Опять же по тем же отозванным материалам Bloomberg, в 2015 году новое подразделение Amazon Web Services (AWS), созданное для нужд ЦРУ, наняла стороннюю компанию для проверки безопасности серверов, поставляемые Elemental, где обнаружили чип вне оригинальной конструкции плат Supermicro. Apple в том же году демонтировал 7000 серверов Supermicro и разорвал контракт на поставку еще 30 тыс. серверов Supermicro.

Затем в сентябре 2015 года, КНР и США пришли к договоренности о правовой поддержке защиты интеллектуальной собственности и увеличении поставок сетевого оборудования из США в Китай. Если эта история на самом деле имело место, то она весьма и весьма поучительна – не шутите ни с каким из централов.

В России же налажен выпуск отечественных маршрутизаторов на процессоре «Байкал-Т1», но по ним пока нет эксплуатационной экспертизы и соответственно компании не готовы к переходу. Также пока нет отечественной серверной ОС и все на свой страх и риск продолжают использовать Windows Server от Miscrosoft и AWS от Amazon, не смотря на высокие корпоративные стандарты онных.

В случае начала театра военных действий, чего конечно ни кто в здравом уме не пожелает, и в силу «чуждости» повсеместно используемых технологий, вполне логично ожидать атаки и попытки уничтожения баз данных крупных государственных и частных организаций страны для нарушения работы экономики.

И обратно, если прилегающая страна, нарушая принципы добрососедства стала размещать у себя системы ПРО-ПВО и авиационные системы противника, то во время боевых действий вероятность «утечки» данных к противнику будет высокой, сколько не из-за желания соседей их передать, а из-за возможного наличия дополнительного чипа.

Поэтому, в силу стратегии национальной безопасности, с первых же секунд начала боевых действий, чисто гипотетически, должна начаться операция по принуждению к миру обведённого вокруг пальца противником соседствующей страны, когда уничтожаются всё вооружение и военная инфраструктура.

К слову, Россия производит модульную военную технику, отчасти чтобы исключить возможность использования дополнительного чипа, чтобы заказчики могли устанавливать свои модули в самые чувствительные места самолёта, корабля, танка или средств ПВО.

Основным способом защиты от системных угроз является внедрение искусственного интеллекта в маршрутизаторы, в средства мониторинга сетей, в кластер Kubernetes, в Spring Security для автоматической блокировки необычной активности. А в случае централизованного управления – блокировка в остальных случаях будет мгновенной по базовым признакам без анализа поведения.

Резюме

Расширение модели безопасности до ABAC не несёт дополнительных затрат, т.к. RBAC естественным образом интегрируется, после чего, информационная безопасность предприятия становится управляемой. Библиотека активно используется и дорабатывается, отчего данная статья будет потихоньку дополнятся. Стоит подключить Z Garbage Collector для хорошего улучшения производительности приложений реактивного стека.

Наиболее эффективная защита от любых типов атак - это централизованный искусственный интеллект уровня государства, который разом защитит все организации. На глобальном уровне все атаки будут видны как на ладони, т.к. визуализируются частые повторы одних и тех же логических шаблонов, как бы их не перетасовывали и не видоизменяли структуру пакетов.

Очень вдохновляет возможность запуска в тестах любого докера – это открывает новую эпоху в тестировании и позволяет значительно усилить тестовое окружение за счёт предустановки дополнительного ПО, что значительно увеличит стабильность всех проектов в целом.

Усиление информационной безопасности Китая привело к значительному ускорению развития отечественных технологий. Было бы естественным рассмотреть существующие стандарты информационной безопасности Китая, что сэкономит немало времени в корректировке обобщённых итоговых целей.

Самый быстрый способ, на мой взгляд, соответствовать общемировым трендам в информационной безопасности – перенести часть производственных мощностей процессоров и системных плат из Китая в Россию для целенаправленного замещения оборудования в госсекторе и смежных областях совместной продукцией. Это так же и самый быстрый способ подойти к производству процессоров техпроцесса от 10-нм и меньше.

Интересная идея, с целью ускорения развития России, построить совместные научно-технические кластера с Китаем и Индией, как с ближайшими союзниками, для выстраивания многополярного технического сотрудничества в охвате сотен стран, в частности, стран Африканского региона и Ближнего Востока. Что приведёт к быстрому техническому развитию стран-участниц союза до мирового уровня из-за эволюционного принципа удешевления применения новых технологий с каждым этапом их развития. Конечной целью союза могло бы стать формирование единого рынка высоких технологий во всех областях экономик стран участниц.

Финансовым институтам России, также с целью ускорения, выгодно наладить партнёрские отношения с IT-гигантами Alibaba и Xiaomi для обоюдного обогащения компетенциями и совместного развития opensource-проектов. Ведь все opensource-проекты мирового уровня, уже признанные в своих областях стандартами де-факто, сосредоточены в Кремниевой долине.

Opensource-проекты часто вытесняют с рынка другие продукты через формирование достаточной критической массы комьюнити вокруг своих технологий. Наличие большого количества экспертов по всему миру и делает продукт стандартом де-факто. В итоге это привело к аккумуляции лучших специалистов планеты в одной стране, т.к. массовая потребность приводит к удовлетворению спроса через дополнительную капитализацию.

Бизнес-модель вокруг opensource-проектов сложная, содержащая множество нюансов и стратегий, требующая немало вложений, но этой модели придерживаются все IT-гиганты Кремниевой долины, т.к. она возникла эволюционно, когда самой выгодной стратегией оказалась "игра в долгую".

Основной стратегией opensource-проектов является высокоуровневая консультация разработчика при доработке продукта сторонними компаниями, когда сходу решаются возникающие проблемы в кодировании. А одной из самых изощренных стратегий является продвижение проекта как платформы для других opensource-проектов более прикладного характера.

Россия обладает лучшими технологиями во многих сферах. Но если сравнивать степень вовлечённости молодых людей в стартапы, то для примера, в Китае созданы более 150 компаний-единорогов с оценочной стоимостью более $1 млрд. и при этом, половина из них выросла до единорога за счёт иностранных инвестиций менее чем за 5 лет. Данный феномен связан с массовым инвестированием государства в стартапы при их создании сразу в акционерный капитал, иначе говоря, инвестирование непосредственно в молодых людей.

Но надо также отметить сильнейшую сторону России как державы. Ведь настоящая мощь всех исторических империй и особенно Российской – в разнообразии способа выражения силы: через культуру, благородство, историческое наследие, взаимное уважение, равенства и поддержки общих интересов, бескорыстия в добрых отношениях, искренней заинтересованности в сильном союзнике и конечно в стремлении к новым знаниям и вершинам.

Теги:
Хабы:
Всего голосов 7: ↑4 и ↓3+1
Комментарии6

Публикации

Истории

Работа

Ближайшие события