Blogical

AWS/Salesforceを中心に様々な情報を配信していきます(/・ω・)/

IoT SiteWise でデータの取り込みをセキュアに行う

こんにちは、ロジカル・アーツの井川です。

今回はメッセージのセキュリティモード/ポリシーを設定して、SiteWise でセキュアにデータを取り込んでみたいと思います。

はじめに

以下の記事で、ユーザ認証を設定して SiteWise でデータを取り込む方法を紹介しました。

blog.logical.co.jp

他にセキュリティ関連の設定として、メッセージのセキュリティモード/ポリシーを設定できるので紹介します。

シナリオ

以前の記事の設定を流用します。EC2 インスタンスの CPU 使用率(%)とメモリ使用量(MB)を取り込む OPC UA サーバがあり、SiteWise はこのサーバからデータ(CPU 使用率とメモリ使用量)を取り込んでいます。このとき、SiteWise と OPC UA サーバ間のデータのやり取りは平文で行われています。

そこで、メッセージのセキュリティモード/ポリシーを設定し、CPU 使用率とメモリ使用量をセキュアに SiteWise に取り込めるようにします。ユーザ認証の続きとして作業される方がいることを想定して構成図や OPC UA サーバのスクリプトにその設定を残していますが、ユーザ認証の設定は必須ではありません。

なお、ゲートウェイの作成手順等はこの記事では扱いません。必要に応じて以下の記事を参考にしてください。
※この記事の手順を試すには、Node.js のオープンソースライブラリ node-opcua で OPC UA サーバを構築しておくことを推奨します。

blog.logical.co.jp

メッセージのセキュリティモード/ポリシー

セキュリティモード

クライアント(ここでは SiteWise)とサーバの接続の署名/暗号化の有無を表します。

以下の 3 種類があります。

  • None(なし)
  • Sign(署名)
  • SignAndEncrypt(署名と暗号化)

セキュリティポリシー

暗号の強度を表します。
SiteWise では以下の 6 種類に対応しています*1

  • None(なし)
  • Basic256Sha256
  • Aes128_Sha256_RsaOaep
  • Aes256_Sha256_RsaPss
  • Basic128Rsa15(非推奨)
  • Basic256(非推奨)

手順

大まかに、以下の作業が必要です。

  • OPC UA サーバ側
    • セキュリティモード/ポリシーの設定
    • アプリケーション証明書を信頼リストに登録
  • SiteWise(クライアント)側
    • セキュリティモード/ポリシーの設定
    • アプリケーション証明書の作成

この記事ではセキュリティモードとポリシーを次のように設定します。

メッセージのセキュリティモード/ポリシーの設定

OPC UA サーバ側

まず、サーバ側のセキュリティモード/ポリシーの設定を行います。

EC2 インスタンスにログインし、以下のコマンドで必要なライブラリをインストールします*2

npm install node-opcua

以下の内容でファイルを作成してください。ファイル名は sample_server.mjs とします。

import {
    DataType,
    OPCUAServer,
    Variant,
    makeRoles,
    WellKnownRoles,
    allPermissions,
    OPCUACertificateManager,
    MessageSecurityMode,
    SecurityPolicy,
} from 'node-opcua';
import * as os from 'os';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// ユーザ情報定義
const USERS = [
    {
        username: 'Demouser',
        password: 'demouserpwd',
        roles: makeRoles([
            WellKnownRoles.AuthenticatedUser,
            WellKnownRoles.ConfigureAdmin,
        ]),
    },
];

let cpuUsage = {
    previousAvgIdel: 0.0,
    previousAvgTick: 0.0,
    get: function () {
        const currentCPUAvg = _getCPUAverage();
        const idelDiff = currentCPUAvg.avgIdle - this.previousAvgIdel;
        const tickDiff = currentCPUAvg.avgTick - this.previousAvgTick;
        const cpuUsage = 100 - (idelDiff / tickDiff) * 100;
        console.log(`CPU Usage: ${cpuUsage}%`);
        this.previousAvgIdel = currentCPUAvg.avgIdle;
        this.previousAvgTick = currentCPUAvg.avgTick;
        return cpuUsage;
    },
};
const { avgIdle, avgTick } = _getCPUAverage();
cpuUsage.previousAvgIdel = avgIdle;
cpuUsage.previousAvgTick = avgTick;

(async () => {
    try {
        // ユーザマネージャーの定義
        const userManager = {
            // ユーザ名とパスワードが正しければ `true`
            isValidUser: (username, password) => {
                const index = USERS.findIndex(
                    (x) => x.username == username && x.password == password
                );
                return index > -1 ? true : false;
            },
            // ユーザに応じたロール(権限)を取得
            getUserRoles: (user) => {
                const index = USERS.findIndex((x) => x.username == user);
                return index > -1 ? USERS[index].roles : [];
            },
        };
        // Certificate マネージャーの定義
        const serverCertManager = new OPCUACertificateManager({
            automaticallyAcceptUnknownCertificate: false,
            rootFolder: join(__dirname, 'node-opcua-default'),
        });
        // endpoint is opc.tcp://<hostname>:4334/UA/Server
        const server = new OPCUAServer({
            port: 4334,
            resourcePath: '/UA/Server',
            allowAnonymous: false, // 匿名ユーザのアクセスを禁止
            userManager: userManager, // 上で定義したユーザマネージャー
            securityModes: [MessageSecurityMode.SignAndEncrypt], // セキュリティモードの定義(署名と暗号化)
            securityPolicies: [SecurityPolicy.Basic256Sha256], // セキュリティポリシーの定義(Basic256Sha256)
            serverCertificateManager: serverCertManager, // Certificate マネージャーの定義
        });

        await server.initialize();

        const addressSpace = server.engine.addressSpace;
        const namespace = addressSpace.getOwnNamespace();

        namespace.setDefaultRolePermissions([
            {
                roleId: WellKnownRoles.AuthenticatedUser,
                permissions: allPermissions,
            },
        ]);

        const device = namespace.addObject({
            organizedBy: addressSpace.rootFolder.objects,
            browseName: 'SampleObject',
        });

        namespace.addVariable({
            componentOf: device,
            browseName: 'CPU',
            dataType: DataType.Double,
            minimumSamplingInterval: 1000,
            value: {
                get: () =>
                    new Variant({
                        dataType: DataType.Double,
                        value: cpuUsage.get(),
                    }),
            },
        });
        namespace.addVariable({
            componentOf: device,
            browseName: 'Memory',
            dataType: DataType.Double,
            minimumSamplingInterval: 1000,
            value: {
                get: () =>
                    new Variant({
                        dataType: DataType.Double,
                        value: getMemoryUsageMB(),
                    }),
            },
        });

        await server.start();
        console.log('OPC Server is now starting ...');
        console.log(
            'endpoint: ',
            server.endpoints[0].endpointDescriptions()[0].endpointUrl
        );
    } catch (err) {
        console.log(err);
        process.exit(1);
    }
})();

function _getCPUAverage() {
    const cpus = os.cpus();
    let totalIdel = 0;
    let totalTick = 0;
    for (const cpu of cpus) {
        for (const type in cpu.times) {
            totalTick += cpu.times[type];
        }
        totalIdel += cpu.times.idle;
    }
    return {
        avgIdle: totalIdel / cpus.length,
        avgTick: totalTick / cpus.length,
    };
}

function getMemoryUsageMB() {
    const memoryUsage = (os.totalmem() - os.freemem()) / 1024 ** 2;
    console.log(`Memory Used: ${memoryUsage}MB`);
    return memoryUsage;
}

一部ユーザ認証の設定を含んでいますが、ユーザ認証記事の手順を試されていない方は不要です。ユーザ認証を使用しない場合は、以下のように上記コードのサーバ定義箇所の allowAnonymoususerManager の部分をコメントアウトしてください。

const server = new OPCUAServer({
    port: 4334,
    resourcePath: '/UA/Server',
    // allowAnonymous: false, // 匿名ユーザのアクセスを禁止
    // userManager: userManager, // 上で定義したユーザマネージャー
    securityModes: [MessageSecurityMode.SignAndEncrypt], // セキュリティモードの定義(署名と暗号化)
    securityPolicies: [SecurityPolicy.Basic256Sha256], // セキュリティポリシーの定義(Basic256Sha256)
    serverCertificateManager: serverCertManager, // Certificate マネージャーの定義
});

ここまで準備ができたら node sample_server.mjs でサーバを起動しておきます。

SiteWise 側

SiteWise のコンソール画面からゲートウェイの詳細画面へ行き、データソースを選択し「編集」をクリックします。

アドバンスト設定」をクリックし、以下のように設定して「保存」をクリックします。

項目 設定値 備考
メッセージのセキュリティモード Basic256Sha256 - 署名と暗号化

この時点ではまだデータを取り込むことができません。SiteWise(クライアント)側で証明書を作成し、OPC UA サーバの信頼リストに配置する作業が必要です。

証明書の作成

SiteWise ゲートウェイプラットフォーム(この記事では EC2)上に、SiteWise が OPC UA サーバ(データソース)と通信したタイミングで PFX ファイルが生成されます。この PFX ファイルから証明書を作成します。なお、PFX ファイル生成には OPC UA サーバが起動している必要があります。まだの場合は、EC2 インスタンスに接続し、node sample_server.mjs でサーバを起動させます。

以下のディレクトリに aws-iot-opcua-client.pfx ファイルがあると思います。

<greengrass root>/work/aws.iot.SiteWiseEdgeCollectorOpcua/<データソース名>/opcua-certificate-store

<greengrass root><データソース名> は設定によるので、読み替えてください。
この記事の設定では

  • <greengrass root>=/greengrass/v2
  • <データソース名>=demo-server

となっているので、次のようになります。

/greengrass/v2/work/aws.iot.SiteWiseEdgeCollectorOpcua/demo-server/opcua-certificate-store

以下のコマンドで証明書を作成します*3

sudo keytool -exportcert -v -alias aws-iot-opcua-client -keystore \
<aws-iot-opcua-client.pfx への絶対パス> \
-storepass amazon -storetype PKCS12 -rfc > aws-iot-opcua-client-certificate.pem

信頼リストへ登録

作成した証明書を OPC UA サーバの信頼リストに追加します。

sample_server.mjs ファイルと同じディレクトリに node-opcua-default/trusted/cert フォルダが作成されていると思います。ここに作成した証明書(aws-iot-opcua-client-certificate.pem)を配置します。

さらに node-opcua-default/rejected/cert フォルダ(拒否リスト)に AWS IoT SiteWise Gateway Client[***].pem という名前*4の PEM ファイルが存在していると思うのでこれを削除します。

これで通信ができるようになると思います。

実際にデータストリームで確認すると、データを取り込めていることが分かります!

うまくいかないとき

拒否リストにある証明書は実はクライアントから渡された証明書なので、これを信頼リストに移動させると、そのクライアントと通信できるようになります。

この記事では、node-opcua-default/rejected/cert にある AWS IoT SiteWise Gateway Client[***].pemnode-opcua-default/trusted/cert に移動させれば通信できるようになると思います。

おわりに

ユーザ認証に引き続き、SiteWise のセキュリティ関連の設定をご紹介しました。これで SiteWise のセキュリティベストプラクティスのうち、OPC UA サーバに関連したものは記事にできたかと思います。実は証明書作成の手順は、公式ドキュメントにはゲートウェイが Greengrass V1 上で動作するときのものしかなく、AWS サポートに確認したところ、Greengrass V2 のときは PFX ファイルの生成場所が変わっていることが分かりました。Greengrass V2 版の手順は調べてもなさそうだったので、この記事が参考になれば幸いです。

参考

*1:https://docs.aws.amazon.com/ja_jp/iot-sitewise/latest/userguide/config-opcua-source-console.html

*2:既にインストール済みであればこの手順はスキップできます。

*3:ここでは -rfc オプションを付け、PEM ファイルを生成していますが、OPC UA サーバによっては DER ファイルでないとうまくいかないことがありましたので、使用する OPC UA サーバのマニュアル等を確認し、必要であればコマンドの一部を修正してください。もしくは、うまくいかないときを参照してください。

*4:*** の箇所はおそらくクライアントごとに固定の文字列になっています。