Technology Engineering

178inaba の技術ブログ

CentOS 7のファイアウォールで80番ポートを許可する

inabaです。

サーバにnginxをインストールして、localhostからはアクセスできるのに、外部(グローバルIPや設定したドメイン)からはアクセスできなくて困っている人はファイアウォールの設定を疑ってみるとよいです。

firewall-cmd

ファイアウォールを操作するコマンドです。

www.firewalld.org

コマンドの使い方を全て覚えるのは大変なのでポート許可に必要な使い方を紹介していきます。

アクティブなゾーンを確認する

ファイアウォールは「事前に定義されたゾーンに対して、指定されたルールの通信を通す。」という事をしています。
なので、まずはどのゾーンへのファイアウォールが動いているかを確認します。

$ firewall-cmd --get-active-zones
public
  interfaces: eth0

--get-active-zonesというオプションをつけると、現在アクティブなゾーンが表示されます。

上記だとpublicというゾーンがアクティブな事がわかります。
複数ある場合は、eth0が外部公開されているインタフェースなのでeth0が定義されているゾーンが外部向けポートを追加する対象になります。

許可されているポートの確認

次は先程確認したアクティブなゾーンpublicの確認です。
現在許可されているポートを確認します。

$ firewall-cmd --info-zone public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client ssh
  ports:
  protocols:
  masquerade: no
  forward-ports:
  sourceports:
  icmp-blocks:
  rich rules:

servicesには定義されているサービス(ルールのような物)が表示されます。
portsにはサービスとは別に開放されているポートが表示されます。
今回はportsには何も表示されていませんが、例えば8080番ポートが開放されていると、ports: 8080/tcpのように表示されます。

上の例だとservicesにdhcpv6-clientとsshが設定されています。

サービスの内容を確認します。

$ firewall-cmd --info-service dhcpv6-client
dhcpv6-client
  ports: 546/udp
  protocols:
  source-ports:
  modules:
  destination: ipv6:fe80::/64
$ firewall-cmd --info-service ssh
ssh
  ports: 22/tcp
  protocols:
  source-ports:
  modules:
  destination:

portsに開放されているポートが表示されます。
dhcpv6-clientでは546番ポート、sshでは22番ポートが開放されています。

定義されているサービスを確認

サービスの一覧は--get-servicesオプションで確認できます。

$ firewall-cmd --get-services
RH-Satellite-6 amanda-client amanda-k5-client bacula bacula-client ceph ceph-mon dhcp dhcpv6 dhcpv6-client dns docker-registry dropbox-lansync freeipa-ldap freeipa-ldaps freeipa-replication ftp high-availability http https imap imaps ipp ipp-client ipsec iscsi-target kadmin kerberos kpasswd ldap ldaps libvirt libvirt-tls mdns mosh mountd ms-wbt mysql nfs ntp openvpn pmcd pmproxy pmwebapi pmwebapis pop3 pop3s postgresql privoxy proxy-dhcp ptp pulseaudio puppetmaster radius rpc-bind rsyncd samba samba-client sane smtp smtps snmp snmptrap squid ssh synergy syslog syslog-tls telnet tftp tftp-client tinc tor-socks transmission-client vdsm vnc-server wbem-https xmpp-bosh xmpp-client xmpp-local xmpp-server

結構量が多いのですが、この中にhttpというそれっぽいサービスを見つけることができます。
httpサービスの内容を確認します。

$ firewall-cmd --info-service http
http
  ports: 80/tcp
  protocols:
  source-ports:
  modules:
  destination:

ビンゴです!
80番ポートがルールとして定義されています。

サービスをゾーンに追加

httpサービスが80番ポートを開放するサービスという事がわかったので、publicゾーンにhttpサービスを追加します。

$ firewall-cmd --permanent --zone public --add-service http
success
$ firewall-cmd --reload
success

--permanentは設定の永続化。(無いと再起動時に設定が消えます。)
--zone publicはゾーンの指定。
--add-service httpは追加するサービスの指定です。

--add-serviceだけでは設定が反映されないので、設定をした後に--reloadファイアウォールに設定を反映させます。

$ firewall-cmd --info-zone public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client http ssh
  ports:
  protocols:
  masquerade: no
  forward-ports:
  sourceports:
  icmp-blocks:
  rich rules:

設定が反映されました。

サービスに許可したいポートが無い場合

例えば8080等のポートを開けたい場合があると思います。
しかし、サービスには一般的に使用されているポートしか定義されておらず、8080番のポートは定義されていません。
そういう場合は--add-serviceの代わりに--add-portを使います。

$ firewall-cmd --permanent --zone public --add-port 8080/tcp
success

このような感じです。

今日はここまで。

ConoHaでサーバ立ててユーザ追加したりSSHの設定したり

inabaです。

ConoHaが安くてサーバ立ち上げが早くて良さげです。

www.conoha.jp

なのでVPSを借りて、いじって遊ぶ用のサーバを作ります。

参考記事:

qiita.com

大体は上記に書いてある通りに進めます。
使用するOSはCentOS 7です。
ここには気になった事や追加でやった事等を書いていきます。

wheelグループって何?

wheelグループに追加と書かれている箇所があります。

# wheelグループに追加
$ gpasswd -a newuser wheel

wheelグループとはデフォルトでsudoができるグループです。
所属しているとユーザ個別に設定しなくてもsudoができます。 設定は/etc/sudoersに記述されています。

$ cat /etc/sudoers
...
## Allows people in group wheel to run all commands
%wheel  ALL=(ALL)   ALL
...

gpasswd

いつもグループへの追加はusermodでやっていて、gpasswdは知りませんでした。
usermod-a忘れるとグループが置き換えられる事がありますが、gpasswdはその心配が無いので良さげです。

qiita.com

パスワード無しでsudo

先程の/etc/sudoersのwheelグループの設定の下にパスワード無し用の設定がコメントアウトされているのでそのコメントアウトを外し、元の設定をコメントアウトします。
sudoは結構重要な操作なので自分は一旦パスワード有りで使ってみて、面倒になってきたら外そうと思います。

## Allows people in group wheel to run all commands
# %wheel    ALL=(ALL)   ALL

## Same thing without a password
%wheel  ALL=(ALL)   NOPASSWD: ALL

SSHログイン許可の設定はデフォルトを使う

参考サイトのSSHログインを許可の項目ですが、設定ファイル(/etc/ssh/sshd_config)はデフォルトだとこうなっていました。

#RSAAuthentication yes
#PubkeyAuthentication yes
AuthorizedKeysFile  .ssh/authorized_keys

RSAAuthenticationPubkeyAuthenticationコメントアウトされています。
コメントアウトされているという事はデフォルト値が使われるという事。

ここにデフォルト値が書いてあるのですが、

sshd の設定(sshd_config)

RSAAuthenticationPubkeyAuthenticationのデフォルト値はyesです。
自分は極力デフォルトで行けるならデフォルトを使いたい人間なので、設定はいじらず、コメントアウトのまま使います。

SSHキーはローカルにあるものを使う

SSH鍵を作成の項目でホスト側でキーを新たに作ってローカルに秘密鍵をコピーしていますが、自分はローカルにSSHキーが生成してあるのでこれを使います。

ローカル:

$ cat ~/.ssh/id_rsa.pub # 公開鍵
ssh-rsa foobarbaz...
# 出た文字列をコピー

サーバ(rootではなく、新しく作ったユーザで実行。):

$ ssh-keygen # ~/.sshディレクトリを手で作るのが面倒なのでコマンドで作る。
# デフォルトで良いのでEnter3回

$ echo 'ssh-rsa foobarbaz...' > ~/.ssh/authorized_keys # ローカルでコピーした公開鍵を登録

$ chmod 600 ~/.ssh/authorized_keys # パーミッションを設定

ssh-keygenを使っちゃってますが、これは補完が効くコマンドの方が打つのが楽だから使ってます。
作った鍵は今は使わないので、普通にmkdir ~/.sshでもいいです。

authorized_keysパーミッションの設定が重要

これで結構ハマった。
SSHキーを使ってでログインしようとしても、パスワードを聞かれるので、sshのログが書かれる /var/log/secureあたりを調べて

Authentication refused: bad ownership or modes for file /home/newuser/.ssh/authorized_keys

と出てきたのでこれで検索。

takuya-1st.hatenablog.jp

authorized_keysパーミッションオーナー以外の書き込み権限がついているとダメなようだった。
実際にauthorized_keysパーミッションを見てみるとrw-rw-r–になっていたので上記の通りchmod 600 ~/.ssh/authorized_keysパーミッションを設定してやると、パスワードを聞かれなくなった!

ログイン攻撃がヤバい

lastbでログイン失敗履歴が見られるのだが、身に覚えのない失敗が約1000回くらいあった。
怖くなってlastでログイン成功履歴を見たが、そっちには身に覚えの無い履歴は無かったので良かったが、こんなに攻撃されるのかとビビるね。
精神衛生上良くない。

なので、rootからのログインを禁止パスワードによるログインを禁止は絶対やるべき。

/etc/ssh/sshd_config:

PermitRootLogin no
PasswordAuthentication no

まとめ

何点か書きましたが、sshauthorized_keysパーミッション以外は特にハマること無く進められました。
このサーバで何しよっかな〜。

今日はここまで。

bufio.Scannerのエラーと解決策 #golang

こんばんわ。
inabaです。

今日はAtCoder Beginner Contest 055に参加していました。

その時に起こったエラーを調査したので参考までに。

Runtime Error

ある問題を解いて提出したら何件かでRuntime Errorが出た。
Runtime Errorの内容は見られないのでコードをちょこちょこ変えながらデバッグ

原因

読み込み文字列が長いのが原因。
その問題の前提条件には10万文字が最大で入ってくるという事が書いてあったが、bufio.Scannerがデフォルトで最大サイズが決まっている事を知らずハマってた。

最大サイズの定数:

MaxScanTokenSize = 64 * 1024

64 * 1024だから約6万5千文字がデフォルトで読み込める最大文字数。
10万文字なので余裕で超える。
デフォルトを超えるサイズの文字列を(*Scanner).Scanすると(*Scanner).Errbufio.ErrTooLongが返される。

var sc = bufio.NewScanner(os.Stdin)
sc.Split(bufio.ScanWords)
sc.Scan()
if err := sc.Err(); err == bufio.ErrTooLong {
    panic(err)
}

みたいな感じで調査してやっと見つけた。

解決策

(*Scanner).Bufferで最大サイズを指定すると65536文字より大きい文字列も読み込んでくれた。

var sc = bufio.NewScanner(os.Stdin)
sc.Buffer([]byte{}, math.MaxInt64)

こんな感じです。
とりあえずint64の最大値を指定してますが、いきなり指定サイズまでアロケーションされるという事は無く、現行のサイズ*2で読み込みを再試行していくようなのでまぁいいかなと。

src/bufio/scan.go - The Go Programming Language

今日はここまで。

外部キーが設定されているとTRUNCATEできない #MariaDB #MySQL

inabaです。

最近はgolangでDBをいじっています。
テストもテスト用DBを作り、実際に書き込んで行うようにしていました。

テストをするたびにAUTO_INCREMENTの値が増えていく

まぁそうでしょう。
なので、テストデータのクリアをするためにTRUNCATEしようと思ってエラー出てハマりました。

エラー

2つのエラーが出ました。

プレースホルダ

_, err = db.Exec("TRUNCATE TABLE ?", tableName)

上記コードの時は下記のエラーが出ました。

Error 1064: You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near '?' at line 1

テーブル名にプレースホルダは使えないらしい。
元々プレースホルダはキャッシュ目的だからFROM句は変えないですし、
セキュリティ的な使用でも、ユーザの入力をFROM句には使わないから妥当な動きですよね。

外部キー制約

プレースホルダをやめてfmt.Sprintfを使ってやるようにしてみたらエラー内容が変わりました。

_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))

下記がエラー。

Error 1701: Cannot truncate a table referenced in a foreign key constraint ...

これは、外部キー制約によってTRUNCATEができないというエラーでした。

外部キー制約の回避

外部キー制約を回避する方法があります。

_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 0")
_, err = db.Exec(fmt.Sprintf("TRUNCATE TABLE %s", tableName))
_, err = db.Exec("SET FOREIGN_KEY_CHECKS = 1")

上記コードではエラーは出ません。
FOREIGN_KEY_CHECKS0に設定すると、外部キー制約の回避ができます。

別の解決法

AUTO_INCREMENTが初期化されればよいのであれば、DELETEしてからAUTO_INCREMENTをリセットすることもできます。

_, err = db.Exec(fmt.Sprintf("DELETE FROM %s", tableName))
_, err = db.Exec(fmt.Sprintf("ALTER TABLE %s AUTO_INCREMENT = 1", tableName))

これなら2行で済みます。
ただ、子テーブルにデータが入っているとエラーになるので、テストで子テーブルも使っている場合は子テーブルの方から消していってください。

今日はここまで。

【Go】structをJSONにする時、ゼロ値を含む/含まないを切り替える方法と注意点 #golang

inabaです。

Go言語でstructをJSONにする時、ゼロ値を含まない場合はタグにomitemptyをつけますよね。

type stampCard struct {
    Stamp int `json:"stamp,omitempty"`
}

しかし、同じstructを使っている時にゼロ値を含む/含まないを切り替えたい時があります。
具体的には下記のようなstructです。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

上記のstructではInStampRallyがtrueの時だけStampの値をJSONに含めたいです。
しかし、このstructのままだとInStampRallyがtrueでもStampが0なら含まれません。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        int  `json:"stamp,omitempty"`
}

func main() {
    sc := stampCard{InStampRally: true, Stamp: 0}
    jsc, err := json.Marshal(pc)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jsc)) // {"in_stamp_rally":true}
}

解決策

解決策はゼロ値を含む/含まないを切り替えたい値をポインタ化します。

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

これで

  • nilでないならJSONに値を含める
  • nilならJSONに値を含めない

という切り替えができます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    scs := []stampCard{
        {InStampRally: false},
        {InStampRally: true},
    }

    for i, sc := range scs {
        if sc.InStampRally {
            scs[i].Stamp = intPointer(0)
        }
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs)) // [{"in_stamp_rally":false},{"in_stamp_rally":true,"stamp":0}]
}

func intPointer(v int) *int {
    return &v
}

上記サンプルコードではstampCardのスライスをループさせてInStampRallyがtrueなら値が0のintポインターStampに代入しています。
こうすることで下記のようにomitemptyが指定されていてもゼロ値をJSONに出力することができます。

[
  {
    "in_stamp_rally": false
  },
  {
    "in_stamp_rally": true,
    "stamp": 0
  }
]

注意点

rangeループのvalueからポインターを取ると、ループでvalueの値が変わってもポインターの値は同じなので、注意しないと意図しない値が入ってしまいます。
(ポインターの値が同じ理由は、ループのたびに新しいメモリを確保しないためでしょう。)

DBから取得した値をループさせてJSON化する時等、注意が必要です。

例として、下記は順番に0, 1, 2, 3のスタンプ数のスタンプカードができる事を期待しているコードです。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        log.Infof("%p", &stamp)
        sc := stampCard{InStampRally: true, Stamp: &stamp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

しかし、上記の結果は下記になります。

$ go run main.go | jq
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
INFO[0000] 0xc42000a3d0
[
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

まず、ループ中のvalueのポインター値を表示していますが、すべて同じになっています。
その後のJSONは全てのstamp3になってしまいました。
全ての値が最後の値に書き換わってしまっています。

修正後のコード

ループのvalueを一旦新しい変数に入れてその変数のポインターを取れば正確な値をJSONに出力できます。

package main

import (
    "encoding/json"
    "fmt"

    log "github.com/Sirupsen/logrus"
)

type stampCard struct {
    InStampRally bool `json:"in_stamp_rally"`
    Stamp        *int `json:"stamp,omitempty"`
}

func main() {
    stamps := []int{0, 1, 2, 3}

    var scs []stampCard
    for _, stamp := range stamps {
        tmp := stamp // important!!
        sc := stampCard{InStampRally: true, Stamp: &tmp}
        scs = append(scs, sc)
    }

    jscs, err := json.Marshal(scs)
    if err != nil {
        log.Error(err)
    }

    fmt.Println(string(jscs))
}

結果:

[
  {
    "in_stamp_rally": true,
    "stamp": 0
  },
  {
    "in_stamp_rally": true,
    "stamp": 1
  },
  {
    "in_stamp_rally": true,
    "stamp": 2
  },
  {
    "in_stamp_rally": true,
    "stamp": 3
  }
]

今日はここまで。

npm loginは空いているユーザ名を指定するとアカウントを作成する

inabaです。

npm owner addで自分のパッケージにオーナーを追加できると知り、
試すためにログインが必要だったのでnpm loginでログインした時の話。

$ npm login
Username: foobar
Password: 
Email: (this IS public) foobar@example.com

上記を見てもらうとわかりますが、ユーザ名、パスワードの次にEmailの入力があります。
「なんでユーザ名入力してるのにEmailの入力も必要なのかな〜」という疑問が湧いてきたので調べてみた。

実はnpm loginは取得されていないユーザ名を指定すると新しいアカウントを作る挙動になっていた。

github.com

loginという名前だったのでこの挙動にはちょっと驚いた。

loginはnpm adduserエイリアスなんだそう。
上記issueではloginとadduserは別々のコマンドに分割する方向で考えているというような事が書いてあった。

今日はここまで。

git mergeのとき、デフォルトでno-ffになるようにする設定

inabaです。

ブランチのマージ時にマージ対象のブランチでどんな変更があったか知るためにマージコミットが欲しいので--no-ff(No Fast-forward)オプションをつけていました。

$ git merge --no-ff foobar

ですが、設定でデフォルトをno-ffにできることを知りました。

設定

$ git config --global merge.ff false

上記設定をすることでgit merge foobarのみでno-ffのマージになります。

$ git merge foobar
...

$ git log --graph --oneline
*   f84d525 Merge branch 'foobar'
|\  
| * 2d68cba Fix B.
|/  
* a3159f3 Fix A.

参考サイト: Git - git-merge Documentation

問題

しかし、上記のみだとpull時もno-ffでマージコミットができてしまいます。

$ git pull
...

$ git log --graph --oneline
*   73018f7 Merge branch 'master' of github.com:178inaba/test
|\  
| *   f84d525 Merge branch 'foobar'
| |\  
|/ /  
| * 2d68cba Fix B.
|/  
* a3159f3 Fix A.

よく言われることですがpullというのはfetchとリモートブランチのmergeの組み合わせなのでリモートブランチをマージする時に上記no-ffの設定が適用されてしまいます。
これを防ぐためにpull時にはFast-forwardでマージするように設定します。

$ git config --global pull.ff only
$ git log --graph --oneline 
* c7356cb Fix C.

$ git pull
...

$ git log --graph --oneline 
* bdbb39c Fix D.
* c7356cb Fix C.

pullでマージコミットが作られないようになりました。

まとめ

$ git config --global merge.ff false
$ git config --global pull.ff only
$ cat ~/.gitconfig
[merge]
    ff = false
[pull]
    ff = only

今日はここまで。