Введение во взаимную аутентификацию сервисов на Java c TLS/SSL

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.


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


Введение


Если говорить о формате настройки сертификатов для безопасной передачи данных. Как правило, данные действия производят на каком-либо веб-сервере типа nginx или apache, стоящем на входе во внутреннюю сеть компании и dmz. Благодаря ему можно разделить защищенную внутреннюю сеть и внешнюю сеть интернет. Далее, внутри доверенной сети каждый поступает по-разному. Кто-то считает, что все внутренние сервисы могут взаимодействовать друг с другом без каких-то ограничений, и контроль пользователей управляется уже в GUI посредством логина и пароля для конкретного приложения с разграничением ролей в рамках приложения. Кто-то идет дальше, подключая LDAP и используя логин, пароль пользователя из общего хранилища.


Существуют различные протоколы и технологии типа RADIUS, Kerberos или OAuth/OpenID для работы с вопросами аутентификации. Кто-то использует схемы с базовой аунтефикацией, передавая логин и пароль в base64, кто-то использует JsonWebToken, еще существует возможность использования сертификатов для проверки не только сервера и клиента. В результате получается ситуация, что мы формируем защищенное соединение клиента и сервера, в котором шифруем передаваемые данные и доверяем не только серверу, с которого эти данные забираем, но и знаем о том, кто именно забирает эти данные с нашего сервера, так как он предоставляет клиентский сертификат.


В рамках моей работы в ТехЦентре Дойче Банка мы в обязательном порядке для всех межсервисных взаимодействий используем SSL-сертификаты — даже в UAT окружении. В Java используем JKS, как более привычный контейнер сертификатов и паролей для этой системы.


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


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


Терминология


  • SSL (англ. Secure Sockets Layer — уровень защищённых сокетов) — криптографический протокол, использующий асимметричную криптографию для аутентификации ключей обмена, симметричное шифрование для сохранения конфиденциальности, коды аутентификации сообщений для целостности сообщений. Третья версия протокола описана в рабочем предложении RFC-6101. В последующем в SSL была обнаружена уязвимость CVE-2014-3566 в связи на базе. В третьей версии протокола было разработано новое рабочее предложение RFC-5246 протокола, получившего название TLS.
  • TLS (англ. Transport Layer Security — Протокол защиты транспортного уровня) — криптографический протокол, развивающие идеи SSLv3 и закрывающий имеющиеся там уязвимости.

Формат сертификатов


В рамках работы с сертификатами обычно используется контейнер PKCS 12 для хранения ключей и сертификатов, но в рамках Java, в дополнение широко используется проприетарный формат JKS (Java KeyStore). Для работы с хранилищем JDK поставляется с консольной утилитой keytool.


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


keytool -genkey -alias example.com -keyalg RSA -keystore keystore.jks  -keysize 2048

Есть ряд других команд под катом, которые могут быть полезны в работе с JKS и просто с ключами и сертификатами в Java


Примеры полезных команды утилиты keytool
  • Создание запроса сертификата (CSR) для существующего Java keystore
    keytool -certreq -alias example.com -keystore keystore.jks -file example.com.csr
  • Загрузка корневого или промежуточного CA сертификата
    keytool -import -trustcacerts -alias root -file Thawte.crt -keystore keystore.jks
  • Импорт доверенного сертификата
    keytool -import -trustcacerts -alias example.com -file example.com.crt -keystore keystore.jks
  • Генерация сапоподписанного сертификата и keystore
    keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -storepass password -validity 360 -keysize 2048
  • Просмотр сертификата
    keytool -printcert -v -file example.com.crt
  • Проверка списка сертификатов в keystore
    keytool -list -v -keystore keystore.jks
  • Проверка конкретного сертификата по алиасу в keystore
    keytool -list -v -keystore keystore.jks -alias example.com
  • Удаление сертификата из keystore
    keytool -delete -alias example.com -keystore keystore.jks
  • Изменение пароля для keystore
    keytool -storepasswd -new new_storepass -keystore keystore.jks
  • Экспорт сертификата из keystore
    keytool -export -alias example.com -file example.com.crt -keystore keystore.jks
  • Список доверенный корневых сертификатов
    keytool -list -v -keystore $JAVA_HOME/jre/lib/security/cacerts
  • Добавление нового корневого сертификата в trustStore
    keytool -import -trustcacerts -file /path/to/ca/ca.pem -alias CA_ALIAS -keystore $JAVA_HOME/jre/lib/security/cacerts

KeyStore & TrustStore


Говоря о JKS, стоит отметить, что данные файлы могут использоваться как KeyStore так и TrustStore. Это два различных типа хранилищ, которые находятся в JKS файлах. Одно из них (KeyStore) содержит более чувствительную информацию типа приватного ключа, и поэтому требует пароля для доступа к этой информации. В противовес чему TrustStore хранит информацию о доверенных сертификатах, которые конечно же присутствуют в операционной системе. Например, для Linux систем мы сможем их найти в /usr/local/share/ca-certificates/


Но так же эти сертификаты идут в поставке Java в файле cacerts, который по умолчанию расположен в директории java.home\lib\security и имеет пароль по умолчанию changeit.


Данная информация может быть полезна в тех случаях, когда установка JDK/JRE осуществляется в компании централизовано из одного источника, и имеется возможность добавления туда своих доверенных сертификатов компании для prod/uat окружения.


Ниже приведена таблица с некоторыми различиями KeyStore & TrustStore.


Keystore TrustStore
Хранятся ваши приватные ключи и сертификаты (клиентские или серверные) Хранятся доверенные сертификаты (корневые самоподписанные CA root)
Необходим для настойки SSL на сервере Необходим для успешного подключения к серверу на клиентской стороне
Клиент будет хранить свой приватный ключ и сертификат в keystore Сервер будет валидировать клиента при двусторонней аутентификации на основании сертификатов в trustStore
javax.net.ssl.keyStore используется для работы с keystore javax.net.ssl.trustStore используется для работы с trustStore

Подключение SSL к NettyServer


При создании нового проекта, подразумевающего взаимодействие клиента и сервера бинарными данными(protobuf) через защищенные вебсокеты (wss), возник вопрос подключения SSL в netty сервер. Как оказалось, это не представляет особых проблем, достаточно в билдере сетевого интерфейса добавить метод .withSslContext(), в который необходимо передать созданный контекст.


NettyServer.builder()
        .addHttpListener(
        NetworkInterfaceBuilder.forPort(serviceUri.getPort())
            .withSslContext(sslContextFactory.getSslContext()),
        PathHandler.path()
            .addExactPath(serviceUri.getPath(), createServiceHandler()
    )
    .build();

Для того что бы сформировать серверный и клиентский SSL-контекст можно использовать один единственный билдер — SslContextBuilder и его методы — forServer и forClient. У этого билдера надо заполнить ряд обязательных полей, такие как trustManager и keyManager. Эти менеджеры мы можем получить из соответствующий фабрик — TrustManagerFactory и KeyManagerFactory.


 SslContextBuilder.forServer(getKeyManagerFactory())
        .trustManager(getTrustManagerFactory())
        .build();

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


private TrustManagerFactory getTrustManagerFactory() throws Exception {
    final TrustManagerFactory tmFactory = TrustManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    final KeyStore keyStore = KeyStore.getInstance("JKS");
    final InputStream trustStoreFile = getTrustStoreFile();
    keyStore.load(trustStoreFile, trustStorePassword.toCharArray());
    tmFactory.init(keyStore);
    return tmFactory;
}

а для KeyStore при инициализации нам необходимо дополнительно передать пароль от самого ключа.


private KeyManagerFactory getKeyManagerFactory() throws Exception {
    final KeyManagerFactory kmFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    final KeyStore keyStore = KeyStore.getInstance("JKS");
    final InputStream keyStoreFile = getKeyStoreFile();
    keyStore.load(keyStoreFile, keyStorePassword.toCharArray());
    kmFactory.init(keyStore, keyPassword.toCharArray());
    return kmFactory;
}

И в целом это все, что необходимо для добавления SSL в NettyServer.


Подключение SSL в gRPC / RSocket


Если говорить о двунаправленном обмене бинарными данными, современных тенденциях к написанию реактивных приложений, стоит отметить gRPC и RSocket для создания подобных приложений. Но поскольку в основании этих протоколов можно использовать Netty как транспорт, логика конфигурирования останется. Поэтому я не буду уделять этому много внимания.


Подключение SSL в Spring Boot для RestController


Но в мире Java разработки Spring стал де факто стандартом для DI. А вместе с внедрением зависимости люди используют и другие технологии, которые удобно собрать в одном Spring Boot приложении, не расходуя множество времени на конфигурирование всего зоопарка технологий. Конечно же, это приводит к избыточности зависимостей и к увеличению времени загрузки, но упрощает разработку. Куда проще написать аннотацию RestController, чем самому разбираться с тем, как корректно хендлить запросы через сервлеты. А для того, чтобы перенаправить всё взаимодействие через сервлеты в защищённый канал с использованием сертификатов, в Spring Boot есть два пути проcтой и более сложный для кастомных решений.
В первом случае достаточно воспользоваться набором пропертей


server.ssl.key-store-type=JKS
server.ssl.key-store=classpath:cert.jks
server.ssl.key-store-password=changeit
server.ssl.key-alias=key
trust.store=classpath:cert.jks
trust.store.password=changeit

И все будет сделано за вас. Либо, если требуется более кастомная конфигурация, поднятие коннекторов на разных портах с нестандартными настройками и так далее, тогда путь лежит в сторону использования интерфейса WebServerFactoryCustomizer, имплементации которого существуют для всех основных контейнеров будь то Jetty, Tomcat или Undertow.


Поскольку это функциональный интерфейс, его довольно просто можно описать через lambda c параметром типа Connector. Для него мы можем выставить флаг setSecure(true), затем заполнить необходимые параметры для ProtocolHandler-а, выставив ему пути до jks c keystore и trustStore и соответствующие пароли к ним. Например, для tomcat код будет выглядеть подобным образом:


@Bean
WebServerFactoryCustomizer<ConfigurableWebServerFactory> containerCustomizer() throws Exception {
    TomcatConnectorCustomizer customizer;
    String absoluteKeystoreFile = keystoreFile.getAbsolutePath();
    String absoluteTruststoreFile = truststoreFile.getAbsolutePath();
    boolean sslEnabled = checkSslSettings(absoluteKeystoreFile, absoluteTruststoreFile, keystorePass);
    if (sslEnabled) {
        customizer = (connector) -> {
            connector.setPort(port);
            connector.setSecure(true);
            connector.setScheme("https");
            Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
            proto.setSSLEnabled(true);
            proto.setClientAuth(clientAuth);
            proto.setKeystoreFile(absoluteKeystoreFile);
            proto.setKeystorePass(keystorePass);
            proto.setTruststoreFile(absoluteTruststoreFile);
            proto.setTruststorePass(truststorePass);
            proto.setKeystoreType(keyStoreType);
            proto.setKeyAlias(keyAlias);
            proto.setCiphers(chiphers);
        };
    } else {
        customizer = (connector) -> {
            connector.setPort(port);
            connector.setSecure(false);
            if (sslEnabled) {
                log.error("Key- or Trust-store file — {} or {} — is not found, or keystore password is missing, REST service on port {} is disabled", absoluteKeystoreFile, absoluteTruststoreFile, port);
                ((Http11NioProtocol) connector.getProtocolHandler()).setMaxConnections(0);
            }
        };
    }

    return (ConfigurableWebServerFactory factory) -> {
        ConfigurableTomcatWebServerFactory tomcatWebServerFactory = (ConfigurableTomcatWebServerFactory) factory;
        tomcatWebServerFactory.addConnectorCustomizers(customizer);
    };

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


Тестирование TLS/SSL


Для того чтобы провести тестирование реализованного безопасного подключения имеется возможность создать keystore программно, используя классы из пакета java.security.* Это даст возможность тестировать различное поведение системы в случае разных ситуаций типа истекших сертификатов, проверки корректной валидации доверенных сертификатов и так далее.


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


Для упрощения этой задачи можно воспользоваться библиотекой bouncyСastle, которая предоставляет ряд дополнительный возможностей в дополнение к стандартным классам в Java, посвященным криптографии из Java Cryptography Architecture (JCA) и Java Cryptography Extension (JCE).


Некоторые аспекты работы с этой библиотекой присутствуют для kotlin и Java в зеркале их репозитория на github (https://github.com/bcgit/bc-java и https://github.com/bcgit/bc-kotlin).


На верхнем уровне абстракции создание keyStore для целей тестирования может выглядеть следующим образом:


KeyStore generateKeyStore(String password) { 
    X509Certificate2 ca = X509Certificate2Builder()
        .setSubject("CN=CA")
        .build()

     X509Certificate2 int = X509Certificate2Builder()
        .setIssuer(ca)
        .setSubject("CN=Intermediate")
        .build()

    X509Certificate2 cert = X509Certificate2Builder()
        .setIssuer(int)
        .setSubject("CN=Child")
        .setIntermediate(false)
        .build()

    return KeyStoreBuilder()
        .addTrustedCertificate("test-ca", ca)
        .addPrivateKey("test-pk", cert.keyPair, password, asList(cert, int, ca))
        .build()
}

Здесь мы, соответственно, можем увидеть наш доверительный корневой сертификат CA, сертификат cert, который выпущен промежуточным звеном, и нашу пару ключей (приватный и публичный), которые хранятся вместе с сертификатом в поле KeyPair keyPair класса X509Certificate2, расширяющем X509Certificate.


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


Соответственно, остается нюанс в непосредственном написании двух билдеров — X509Certificate2Builder и KeyStoreBuilder. Конечно же в java существует java.security.KeyStore.Builder, но он весьма общего плана и имеет единственный ценный метод — newInstance, а хочется чего-то более явного для добавления доверенных сертификатов и приватных ключей. По этой причине был написан свой билдер.


Свой билдер использует в конечном итоге метод setEntry класса KeyStore для единообразного добавления сущностей доверенных сертификатов и приватных ключей, используя различные имплементации типа Entry (TrustedCertificateEntry или PrivateKeyEntry).


И поскольку KeyStore#setEntry имеет сигнатуру setEntry(String alias, Entry entry, ProtectionParameter protParam) с 3 параметрами, мы их можем объединить в один класс item и в итоге в методе KeyStoreBuilder#build() останется лишь следующий код:


public KeyStore build() {
    try {
        KeyStore keyStore = KeyStore.getInstance("JKS", "SUN");
        keyStore.load(null, null);
        for (Item it : entries.values()) {
            keyStore.setEntry(it.alias, it.entry, it.parameter);
        }
        return keyStore;
    } catch (IOException | GeneralSecurityException e) {
        throw new RuntimeException(e.getMessage(), e);
    }

А при добавлении сущностей в entries мы будем использовать сигнатуру аналогичную KeyStore#setEntry, но публичным интерфейсом, использующим этот setEntry будут более понятные методы addPrivateKey


public KeyStoreBuilder addPrivateKey(String alias, KeyPair pair, String password, List<X509Certificate> chain) {
    addEntry(alias,
             new KeyStore.PrivateKeyEntry(pair.getPrivate(), chain),
             new KeyStore.PasswordProtection(password.toCharArray()));
    return this;
 }

и метод addTrustedCertificate,


public KeyStoreBuilder addTrustedCertificate(String alias, X509Certificate cert) {
    addEntry(alias, new KeyStore.TrustedCertificateEntry(cert), null);
    return this;
}

которые мы использовали выше при генерации keyStore.


C билдером X509 сертификата дела обстоят чуть сложнее, поскольку основная часть логики там будет сосредоточена в методе build(). Чтобы не загромождать статью болейрплейт кодом сеттеров, которые просто устанавливают значения полей билдера, я сразу перейду к реализации метода build() для X509Certificate2, опустив методы, связанные с установкой значений в билдер, и использую вместо них локальные переменные:


public X509Certificate2 build() {
    if (Security.getProvider("BC") == null) {
        Security.addProvider(new BouncyCastleProvider());
    }

    final X500Name subject = new X500Name(this.subject);
    final KeyPair subjectKeyPair = newKeyPair(subjectKeyStrength);
    final boolean selfSigned = this.issuer == null;
    final X500Name issuer = selfSigned ? subject : new X500Name(this.issuer.getSubjectDN().getName());
    final KeyPair issuerKeyPair = selfSigned ? subjectKeyPair : this.issuer.getKeyPair();

    // Create x509 certificate
    final Date notBefore = new Date();
    final Date notAfter = new Date(notBefore.getTime() + 20L * 365 * 24 * 60 * 60 * 1000);
    final BigInteger serialNumber = BigInteger.valueOf(SERIALS.incrementAndGet());
    final SubjectPublicKeyInfo subjectKeyInfo = SubjectPublicKeyInfo.getInstance(subjectKeyPair.getPublic().getEncoded());
    final X509v3CertificateBuilder builder = new X509v3CertificateBuilder(
        issuer, serialNumber, notBefore, notAfter, subject, subjectKeyInfo);

    // Get the certificate back
    final AlgorithmIdentifier sigAlgId = new DefaultSignatureAlgorithmIdentifierFinder().find("SHA256withRSA");
    final AlgorithmIdentifier digAlgId = new DefaultDigestAlgorithmIdentifierFinder().find(sigAlgId);

    try {
        final BcX509ExtensionUtils extensionUtils = new BcX509ExtensionUtils();
        builder.addExtension(
            new ASN1ObjectIdentifier("2.5.29.14"), // Subject Key Identifier
            false,
            extensionUtils.createSubjectKeyIdentifier(
                SubjectPublicKeyInfo.getInstance(subjectKeyPair.getPublic().getEncoded()))
        );
        builder.addExtension(
            new ASN1ObjectIdentifier("2.5.29.35"), // Authority Key Identifier
            false,
            extensionUtils.createAuthorityKeyIdentifier(
                SubjectPublicKeyInfo.getInstance(issuerKeyPair.getPublic().getEncoded()))
        );
        builder.addExtension(
            new ASN1ObjectIdentifier("2.5.29.19"), // Basic Constraints
            false,
            new BasicConstraints(intermediate)); 
        AsymmetricKeyParameter privateKey = PrivateKeyFactory.createKey(issuerKeyPair.getPrivate().getEncoded());
        ContentSigner signer = new BcRSAContentSignerBuilder(sigAlgId, digAlgId)
        .build(privateKey);
        X509Certificate cert = new JcaX509CertificateConverter()
            .setProvider("BC")
            .getCertificate(builder.build(signer));

        return new X509Certificate2(cert, subjectKeyPair);
    } catch (IOException | OperatorCreationException | CertificateException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

private static KeyPair newKeyPair(int subjectKeyStrength) {
    try {
        if (subjectKeyStrength <= 0) {
            subjectKeyStrength = DEFAULT_KEY_STRENGTH; // 2048 
        }

        // Create the public/private rsa key pair
        KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance("RSA", "BC");
        keyPairGen.initialize(subjectKeyStrength, SecureRandom.getInstance("SHA1PRNG"));
        return keyPairGen.generateKeyPair();
    } catch (GeneralSecurityException e) {
        throw new RuntimeException(e.getMessage(), e);
    }
}

Все недостающие в стандартной библиотеке классы импортированы из bouncycastle.
В начале работы необходимо проинициализировать провайдер bouncycastle, если это еще не было сделано ранее.


Пара ключей генерируется с использованием java.security.KeyPairGenerator, который позволяет создать ключи с заданный алгоритмом и хеш-функцией. В данном примере был использован RSA c SHA1PRNG.


Далее мы объявляем поля, необходимые для сертификата, такие как даты начала и окончания, эмитент и серийный номер.


Затем мы добавляем расширения для сертификата, описывающие субъект и указание корневого сертификата, подписавшего его. В конце концов, получаем сертификат.


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


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


В результате, например, для проверки безопасного соединения в Spring boot приложении без использования стандартного пути с пропертями достаточно будет создать шаблонное приложение с вебом, например, через Spring Initializr и


добавить в главный класс следующий код
@SpringBootApplication
@RestController
public class Server {
    static String certFile = System.getProperty("user.home") + "/cert.jks";
    static String defaultPassword = "changeit";

    @GetMapping("/hello")
    public String hello() {
        System.out.println("request /hello");
        return "hello";
    }

    @Bean
    WebServerFactoryCustomizer<ConfigurableWebServerFactory> containerCustomizer() {
        TomcatConnectorCustomizer customizer = (connector) -> {
            connector.setPort(8080);
            connector.setSecure(true);
            connector.setScheme("https");
            Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler();
            proto.setSSLEnabled(true);
            proto.setClientAuth("true");
            proto.setKeystoreFile(certFile);
            proto.setKeystorePass(defaultPassword);
            proto.setTruststoreFile(certFile);
            proto.setTruststorePass(defaultPassword);
            proto.setKeystoreType("JKS");
        };

        return (ConfigurableWebServerFactory factory) -> {
            ConfigurableTomcatWebServerFactory tomcatWebServerFactory = (ConfigurableTomcatWebServerFactory) factory;
            tomcatWebServerFactory.addConnectorCustomizers(customizer);
        };
    }

    public static void main(String[] args) {
        SpringApplication.run(Server.class, args);
    }
}

class Client {
    RestTemplate restTemplate() throws Exception {
        SSLContext sslContext = new SSLContextBuilder()
                .loadTrustMaterial(new URL(Server.certFile), Server.defaultPassword.toCharArray())
                .build();
        SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext);
        HttpClient httpClient = HttpClients.custom()
                .setSSLSocketFactory(socketFactory)
                .build();
        HttpComponentsClientHttpRequestFactory factory =
                new HttpComponentsClientHttpRequestFactory(httpClient);
        return new RestTemplate(factory);
    }

    public static void main(String[] args) {
        String response = new RestTemplate().getForObject("https://localhost:8080/hello", String.class);
        System.out.println("received " + response);
    }
}

В таком примере используется уже готовый cert.jks файл, если надо будет создать его на лету можно воспользоваться примерами, которые я приводил выше и доработать для себя так как будет удобно.

Источник: https://habr.com/ru/company/dbtc/blog/487318/


Интересные статьи

Интересные статьи

Сравнение трех самых популярных Managed Kubernetes платформ. Kubernetes уже стал синонимом оркестрации контейнеров, поэтому каждый облачный провайдер активно разрабатывает под него сво...
Доброго времени суток! Как известно, одной из характерных черт JavaScript, наряду c мультипарадигменностью, слабой (динамической) типизацией, автоматическим управлением памятью и ...
Не любите Java? Да вы не умеете ее готовить! Mani Sarkar предлагает нам познакомиться с инструментом Valohai, позволяющим проводить исследования модели на Java.
Вы когда-нибудь задумывались, как браузеры читают и исполняют JavaScript-код? Это выглядит таинственно, но в этом посте вы можете получить представление, что же происходит под капотом. Начнё...
Всем, всем, всем, преподающим информатику детям лет 10 — 14! По ссылке доступен русский перевод курса «Введение в информатику с MakeCode для Minecraft». По ссылке страница курса у вас скор...